Program Tip

사용하지 않는 문자열의 컴파일러 최적화에 대한 일관성없는 동작

programtip 2020. 11. 5. 18:52
반응형

사용하지 않는 문자열의 컴파일러 최적화에 대한 일관성없는 동작


다음 코드가 왜 궁금합니다.

#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문자열이 1 바이트 더 길어도 여전히 사용되지 않는다는 것을 인식하지 못하는 이유를 이해할 수 없습니다 .

이 질문은 gcc 9.1 및 clang 8.0 (온라인 : https://gcc.godbolt.org/z/p1Z8Ns ) 과 관련이 있습니다. 내 관찰에서 다른 컴파일러가 사용하지 않는 변수 (ellcc)를 완전히 삭제하거나 해당 변수에 대한 코드를 생성하기 때문입니다. 문자열의 길이.


이것은 작은 문자열 최적화 때문입니다. 문자열 데이터가 null 종결자를 포함하여 16 자보다 작거나 같으면 std::string개체 자체의 로컬 버퍼에 저장 됩니다. 그렇지 않으면 힙에 메모리를 할당하고 거기에 데이터를 저장합니다.

첫 번째 문자열 "ABCDEFGHIJKLMNO"과 null 종결자는 정확히 크기가 16입니다. 추가 "P"하면 버퍼를 초과하므로 new내부적으로 호출되어 불가피하게 시스템 호출로 이어집니다. 컴파일러는 부작용이 없는지 확인하는 것이 가능하다면 무언가를 최적화 할 수 있습니다. 시스템 호출은 아마도 이것을 불가능하게 만들 것입니다. constrast에 의해 생성중인 객체에 로컬 버퍼를 변경하면 그러한 부작용 분석이 가능합니다.

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;
  }

로컬 버퍼가 내용으로 채워지는 곳. 이 부분 직후, 로컬 용량이 고갈 된 지점에 도달합니다. 새 스토리지가 할당되고 (에서 할당을 통해 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;
  }

As a side note, small string optimization is quite a topic on its own. To get a feeling for how tweaking individual bits can make a difference at large scale, I'd recommend this talk. It also mentions how the std::string implementation that ships with gcc (libstdc++) works and changed during the past to match newer versions of the standard.


I was surprised the compiler saw through a std::string constructor/destructor pair until I saw your second example. It didn't. What you're seeing here is small string optimization and corresponding optimizations from the compiler around that.

Small string optimizations are when the std::string object itself is big enough to hold the contents of the string, a size and possibly a discriminating bit used to indicate whether the string is operating in small or big string mode. In such a case, no dynamic allocations occur and the string is stored in the std::string object itself.

Compilers are really bad at eliding unneeded allocations and deallocations, they are treated almost as if having side effects and are thus impossible to elide. When you go over the small string optimization threshold, dynamic allocations occur and the result is what you see.

As an example

void foo() {
    delete new int;
}

is the simplest, dumbest allocation/deallocation pair possible, yet gcc emits this assembly even under 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)

참고URL : https://stackoverflow.com/questions/56425276/inconsistent-behavior-of-compiler-optimization-of-unused-string

반응형