Непоследовательное поведение оптимизации компилятора неиспользуемой строки - PullRequest
69 голосов
/ 03 июня 2019

Мне любопытно, почему следующий фрагмент кода:

#include <string>
int main()
{
    std::string a = "ABCDEFGHIJKLMNO";
}

при компиляции с -O3 дает следующий код:

main:                                   # @main
    xor     eax, eax
    ret

(я прекрасно понимаю, что нетнеобходимо неиспользованное a, чтобы компилятор мог полностью исключить его из сгенерированного кода)

Однако следующая программа:

#include <string>
int main()
{
    std::string a = "ABCDEFGHIJKLMNOP"; // <-- !!! One Extra P 
}

дает:

main:                                   # @main
        push    rbx
        sub     rsp, 48
        lea     rbx, [rsp + 32]
        mov     qword ptr [rsp + 16], rbx
        mov     qword ptr [rsp + 8], 16
        lea     rdi, [rsp + 16]
        lea     rsi, [rsp + 8]
        xor     edx, edx
        call    std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_create(unsigned long&, unsigned long)
        mov     qword ptr [rsp + 16], rax
        mov     rcx, qword ptr [rsp + 8]
        mov     qword ptr [rsp + 32], rcx
        movups  xmm0, xmmword ptr [rip + .L.str]
        movups  xmmword ptr [rax], xmm0
        mov     qword ptr [rsp + 24], rcx
        mov     rax, qword ptr [rsp + 16]
        mov     byte ptr [rax + rcx], 0
        mov     rdi, qword ptr [rsp + 16]
        cmp     rdi, rbx
        je      .LBB0_3
        call    operator delete(void*)
.LBB0_3:
        xor     eax, eax
        add     rsp, 48
        pop     rbx
        ret
        mov     rdi, rax
        call    _Unwind_Resume
.L.str:
        .asciz  "ABCDEFGHIJKLMNOP"

при компиляции с таким же -O3.Я не понимаю, почему он не распознает, что a все еще не используется, несмотря на то, что строка длиннее на один байт.

Этот вопрос относится к gcc 9.1 и clang 8.0, (онлайн: https://gcc.godbolt.org/z/p1Z8Ns), потому что другие компиляторы в моем наблюдении либо полностью отбрасывают неиспользуемую переменную (ellcc), либо генерируют код для нее независимо от длины строки.

Ответы [ 2 ]

65 голосов
/ 03 июня 2019

Это связано с небольшой оптимизацией строки.Когда строковые данные меньше или равны 16 символам, включая нулевой терминатор, они сохраняются в буфере, локальном для самого объекта std::string.В противном случае он выделяет память в куче и сохраняет там данные.

Первая строка "ABCDEFGHIJKLMNO" плюс нулевой терминатор точно имеет размер 16. Добавление "P" делает его превышающим буфер, следовательно new вызывается изнутри, неизбежно приводя к системному вызову.Компилятор может что-то оптимизировать, если есть возможность гарантировать отсутствие побочных эффектов.Системный вызов, вероятно, делает невозможным это сделать - благодаря ограничению, изменение локального буфера для строящегося объекта допускает такой анализ побочных эффектов.

Отслеживание локального буфера в libstdc ++, версия 9.1, раскрывает эти частиbits/basic_string.h:

template<typename _CharT, typename _Traits, typename _Alloc>
class basic_string
{
   // ...

  enum { _S_local_capacity = 15 / sizeof(_CharT) };

  union
    {
      _CharT           _M_local_buf[_S_local_capacity + 1];
      size_type        _M_allocated_capacity;
    };
   // ...
 };

, что позволяет определить размер локального буфера _S_local_capacity и сам локальный буфер (_M_local_buf).Когда конструктор инициирует вызов basic_string::_M_construct, у вас есть в bits/basic_string.tcc:

void _M_construct(_InIterator __beg, _InIterator __end, ...)
{
  size_type __len = 0;
  size_type __capacity = size_type(_S_local_capacity);

  while (__beg != __end && __len < __capacity)
  {
    _M_data()[__len++] = *__beg;
    ++__beg;
  }

, где локальный буфер заполнен его содержимым.Сразу после этой части мы попадаем в ветку, где исчерпана локальная емкость - выделяется новое хранилище (через allocate в M_create), локальный буфер копируется в новое хранилище и заполняется оставшейся частью инициализирующего аргумента:

  while (__beg != __end)
  {
    if (__len == __capacity)
      {
        // Allocate more space.
        __capacity = __len + 1;
        pointer __another = _M_create(__capacity, __len);
        this->_S_copy(__another, _M_data(), __len);
        _M_dispose();
        _M_data(__another);
        _M_capacity(__capacity);
      }
    _M_data()[__len++] = *__beg;
    ++__beg;
  }

В качестве примечания, небольшая оптимизация строк - отдельная тема.Чтобы понять, как настройка отдельных битов может иметь большое значение, я бы порекомендовал этот доклад .В нем также упоминается, как работает реализация std::string, поставляемая с gcc (libstdc ++), и измененная в прошлом для соответствия более новым версиям стандарта.

19 голосов
/ 03 июня 2019

Я был удивлен, что компилятор просматривал пару std::string конструктор / деструктор, пока не увидел ваш второй пример. Это не так. Здесь вы видите небольшую строковую оптимизацию и соответствующие оптимизации от компилятора.

Оптимизация небольших строк - это когда большой объект std::string достаточно большой, чтобы содержать содержимое строки, размер и, возможно, различающий бит, используемый для указания того, работает ли строка в режиме маленькой или большой строки. В таком случае динамическое распределение не происходит, и строка сохраняется в самом объекте std::string.

Компиляторы действительно плохи в устранении ненужных выделений и освобождений, они рассматриваются почти как имеющие побочные эффекты и, следовательно, их невозможно исключить. Когда вы превышаете порог оптимизации небольшой строки, происходит динамическое распределение, и в результате вы видите то, что видите.

Как пример

void foo() {
    delete new int;
}

- самая простая и самая тупая пара размещения / освобождения, однако gcc испускает эту сборку даже при O3

sub     rsp, 8
mov     edi, 4
call    operator new(unsigned long)
mov     esi, 4
add     rsp, 8
mov     rdi, rax
jmp     operator delete(void*, unsigned long)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...