编译器在添加字符时停止优化未使用的字符串
Compiler stops optimizing unused string away when adding characters
我很好奇为什么会出现下面这段代码:
#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),要么为它生成代码,而不管字符串。
这是由于小字符串优化。当字符串数据小于或等于 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;
}
本地缓冲区被其内容填充的位置。在这部分之后,我们到达本地容量耗尽的分支 - 分配新存储(通过 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;
}
附带说明一下,小字符串优化本身就是一个话题。为了感受调整单个位如何在大规模上产生影响,我建议 this talk。它还提到 gcc
(libstdc++) 附带的 std::string
实现在过去如何工作和更改以匹配标准的新版本。
令我惊讶的是,在我看到您的第二个示例之前,编译器能够看穿 std::string
constructor/destructor 对。它没有。你在这里看到的是小字符串优化和编译器围绕它的相应优化。
小字符串优化是指当 std::string
对象本身大到足以容纳字符串的内容、大小和可能用于指示字符串是在小字符串模式还是大字符串模式下运行的区分位时.在这种情况下,不会发生动态分配,字符串存储在 std::string
对象本身中。
编译器确实不擅长消除不必要的分配和解除分配,它们几乎被视为具有副作用,因此无法消除。当您超过小字符串优化阈值时,会发生动态分配,结果如您所见。
举个例子
void foo() {
delete new int;
}
是最简单、最笨的 allocation/deallocation 对,但 gcc emits 即使在 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)
虽然接受的答案是有效的,但从 C++14 开始,实际上 new
和 delete
调用 可以 被优化掉。在 cppreference 上看到这个神秘的措辞:
New-expressions are allowed to elide ... allocations made through replaceable allocation functions. In case of elision, the storage may be provided by the compiler without making the call to an allocation function (this also permits optimizing out unused new-expression).
...
Note that this optimization is only permitted when new-expressions are
used, not any other methods to call a replaceable allocation function:
delete[] new int[10];
can be optimized out, but operator
delete(operator new(10));
cannot.
这实际上允许编译器完全删除您的本地 std::string
,即使它很长。事实上 - clang++ 与 libc++ already does this (GodBolt),因为 libc++ 在其 std::string
的实现中使用内置 __new
和 __delete
- 即 "storage provided by the compiler"。因此,我们得到:
main():
xor eax, eax
ret
基本上是任意长度的未使用字符串。
GCC 没有,但我最近打开了有关此的错误报告;请参阅 this SO answer 以获取链接。
我很好奇为什么会出现下面这段代码:
#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),要么为它生成代码,而不管字符串。
这是由于小字符串优化。当字符串数据小于或等于 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; }
本地缓冲区被其内容填充的位置。在这部分之后,我们到达本地容量耗尽的分支 - 分配新存储(通过 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; }
附带说明一下,小字符串优化本身就是一个话题。为了感受调整单个位如何在大规模上产生影响,我建议 this talk。它还提到 gcc
(libstdc++) 附带的 std::string
实现在过去如何工作和更改以匹配标准的新版本。
令我惊讶的是,在我看到您的第二个示例之前,编译器能够看穿 std::string
constructor/destructor 对。它没有。你在这里看到的是小字符串优化和编译器围绕它的相应优化。
小字符串优化是指当 std::string
对象本身大到足以容纳字符串的内容、大小和可能用于指示字符串是在小字符串模式还是大字符串模式下运行的区分位时.在这种情况下,不会发生动态分配,字符串存储在 std::string
对象本身中。
编译器确实不擅长消除不必要的分配和解除分配,它们几乎被视为具有副作用,因此无法消除。当您超过小字符串优化阈值时,会发生动态分配,结果如您所见。
举个例子
void foo() {
delete new int;
}
是最简单、最笨的 allocation/deallocation 对,但 gcc emits 即使在 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)
虽然接受的答案是有效的,但从 C++14 开始,实际上 new
和 delete
调用 可以 被优化掉。在 cppreference 上看到这个神秘的措辞:
New-expressions are allowed to elide ... allocations made through replaceable allocation functions. In case of elision, the storage may be provided by the compiler without making the call to an allocation function (this also permits optimizing out unused new-expression).
...
Note that this optimization is only permitted when new-expressions are used, not any other methods to call a replaceable allocation function:
delete[] new int[10];
can be optimized out, but operatordelete(operator new(10));
cannot.
这实际上允许编译器完全删除您的本地 std::string
,即使它很长。事实上 - clang++ 与 libc++ already does this (GodBolt),因为 libc++ 在其 std::string
的实现中使用内置 __new
和 __delete
- 即 "storage provided by the compiler"。因此,我们得到:
main():
xor eax, eax
ret
基本上是任意长度的未使用字符串。
GCC 没有,但我最近打开了有关此的错误报告;请参阅 this SO answer 以获取链接。