사용하지 않는 문자열의 컴파일러 최적화에 대한 일관성없는 동작
다음 코드가 왜 궁금합니다.
#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)
'Program Tip' 카테고리의 다른 글
ApplicationContextAware는 Spring에서 어떻게 작동합니까? (0) | 2020.11.05 |
---|---|
IPython / Jupyter 노트북을 PDF로 저장할 때 발생하는 문제 (0) | 2020.11.05 |
D3.js가 데이터를 노드에 바인딩하는 방법 이해 (0) | 2020.11.05 |
곱셈이 부동 나누기보다 빠릅니까? (0) | 2020.11.05 |
자바 스크립트 약속을 디버그하는 방법은 무엇입니까? (0) | 2020.11.05 |