我很好奇为什么下面的代码:
#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),要么为该变量生成代码,而与字符串的长度无关。
答案 0 :(得分:65)
这是由于小字符串优化。当字符串数据少于或等于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。它还提到了std::string
附带的gcc
实现(libstdc ++)如何工作并在过去进行了更改以匹配该标准的较新版本。
答案 1 :(得分:19)
令我惊讶的是,编译器看到了std::string
构造函数/析构函数对,直到看到您的第二个示例。没有。您在这里看到的是小字符串优化以及编译器对此进行的相应优化。
小型字符串优化是指std::string
对象本身足够大以容纳字符串的内容,大小以及可能用来表示字符串是在小字符串模式还是大字符串模式下使用的区分位。在这种情况下,不会发生动态分配,并且字符串将存储在std::string
对象本身中。
编译器在取消不必要的分配和释放方面非常不好,它们几乎被视为具有副作用,因此无法排除。当您超过小型字符串优化阈值时,就会发生动态分配,结果就是您所看到的。
例如
void foo() {
delete new int;
}
是可能的最简单,最愚蠢的分配/取消分配对,但即使在O3下,gcc emits仍会装配该组件
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)
答案 2 :(得分:0)
尽管接受的答案是有效的,但是从C ++ 14开始,实际上可以优化new
和delete
调用 的情况。请参阅有关cppreference的奥秘用语:
允许新表达式取消通过可替换分配函数进行的分配。在省略的情况下,编译器可以在不调用分配函数的情况下提供存储(这也可以优化未使用的new表达式)。
...
请注意,只有在new-expressions为 而不是其他任何方法来调用可替换的分配函数:
delete[] new int[10];
可以优化,但运算符delete(operator new(10));
不能。
这实际上允许编译器完全删除本地std::string
,即使它很长。实际上-带有libc ++ already does this的clang ++(GodBolt),因为libc ++在__new
的实现中使用内置__delete
和std::string
-这就是“编译器提供的存储” 。因此,我们得到:
main():
xor eax, eax
ret
基本上具有任何长度的未使用字符串。
GCC不会,但是我最近打开了有关此问题的错误报告;请参阅this SO answer以获取链接。