添加字符时,编译器停止优化未使用的字符串 [英] Compiler stops optimizing unused string away when adding characters
问题描述
我很好奇以下代码为何:
I am curious why the following piece of code:
#include <string>
int main()
{
std::string a = "ABCDEFGHIJKLMNO";
}
当使用 -O3 $ c $进行编译时c>产生以下代码:
main: # @main
xor eax, eax
ret
(我完全理解,不需要未使用的 a
,以便编译器可以完全从生成的代码中忽略它)
(I perfectly understand that there is no need for the unused a
so the compiler can entirely omit it from the generated code)
但是以下程序:
#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
仍未使用,无论字符串长了一个字节如何。
when compiled with the same -O3
. I don't understand why it does not recognize that the a
is still unused, regardless that the string is one byte longer.
此问题与gcc 9.1和clang 8.0有关,(在线: https://gcc.godbolt .org / z / p1Z8Ns ),因为在我看来,其他编译器要么完全丢弃未使用的变量(ellcc),要么为该变量生成代码,而不管字符串的长度如何。
This question is relevant to gcc 9.1 and clang 8.0, (online: https://gcc.godbolt.org/z/p1Z8Ns) because other compilers in my observation either entirely drop the unused variable (ellcc) or generate code for it regardless the length of the string.
推荐答案
这是由于小字符串优化所致。当字符串数据少于或等于16个字符(包括空终止符)时,它将存储在 std :: string
对象本身局部的缓冲区中。否则,它将在堆上分配内存并将数据存储在堆上。
This is due to the small string optimization. When the string data is less than or equal 16 characters, including the null terminator, it is stored in a buffer local to the std::string
object itself. Otherwise, it allocates memory on the heap and stores the data over there.
第一个字符串 ABCDEFGHIJKLMNO
空终止符的大小正好为16。添加 P
使其超过缓冲区,因此将调用 new
在内部,不可避免地导致系统调用。如果可以确保没有副作用,编译器可以优化一些东西。系统调用可能无法做到这一点-通过进行约束,更改正在构造的对象的本地缓冲区可以进行这种副作用分析。
The first string "ABCDEFGHIJKLMNO"
plus the null terminator is exactly of size 16. Adding "P"
makes it exceed the buffer, hence new
is being called internally, inevitably leading to a system call. The compiler can optimize something away if it's possible to ensure that there are no side effects. A system call probably makes it impossible to do this - by constrast, changing a buffer local to the object under construction allows for such a side effect analysis.
跟踪本地缓冲区在libstdc ++版本9.1中,显示了 bits / basic_string.h
的以下部分:
Tracing the local buffer in libstdc++, version 9.1, reveals these parts of 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
:
which lets you spot the local buffer size _S_local_capacity
and the local buffer itself (_M_local_buf
). When the constructor triggers basic_string::_M_construct
being called, you have in 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
中的分配),将本地缓冲区复制到新的存储并填充其余的初始化参数:
where the local buffer is filled with its content. Right after this part, we get to the branch where the local capacity is exhausted - new storage is allocated (through the allocate in M_create
), the local buffer is copied into the new storage and filled with the rest of the initializing argument:
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;
}
作为小注释,小字符串优化它本身就是一个话题。为了了解单个位的调整如何在很大程度上产生影响,我建议这个话题。它还提到了 gcc
(libstdc ++)附带的 std :: string
实现是如何工作的,并在过去更改为匹配标准的较新版本。
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.
这篇关于添加字符时,编译器停止优化未使用的字符串的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!