未使用的字符串的编译器优化行为不一致

时间:2019-06-03 10:17:08

标签: c++ gcc compilation clang compiler-optimization

我很好奇为什么下面的代码:

#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),要么为该变量生成代码,而与字符串的长度无关。

3 个答案:

答案 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开始,实际上可以优化newdelete调用 的情况。请参阅有关cppreference的奥秘用语:

允许新表达式取消通过可替换分配函数进行的分配。在省略的情况下,编译器可以在不调用分配函数的情况下提供存储(这也可以优化未使用的new表达式)。

...

请注意,只有在new-expressions为 而不是其他任何方法来调用可替换的分配函数: delete[] new int[10];可以优化,但运算符 delete(operator new(10));不能。

这实际上允许编译器完全删除本地std::string,即使它很长。实际上-带有libc ++ already does this的clang ++(GodBolt),因为libc ++在__new的实现中使用内置__deletestd::string-这就是“编译器提供的存储” 。因此,我们得到:

main():
        xor eax, eax
        ret

基本上具有任何长度的未使用字符串。

GCC不会,但是我最近打开了有关此问题的错误报告;请参阅this SO answer以获取链接。