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

Fer*_*eak 69 c++ gcc compilation clang compiler-optimization

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

#include <string>
int main()
{
    std::string a = "ABCDEFGHIJKLMNO";
}
Run Code Online (Sandbox Code Playgroud)

当使用编译时,将-O3产生以下代码:

main:                                   # @main
    xor     eax, eax
    ret
Run Code Online (Sandbox Code Playgroud)

(我完全理解不需要多余的,a因此编译器可以从生成的代码中完全忽略它)

但是以下程序:

#include <string>
int main()
{
    std::string a = "ABCDEFGHIJKLMNOP"; // <-- !!! One Extra P 
}
Run Code Online (Sandbox Code Playgroud)

产量:

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"
Run Code Online (Sandbox Code Playgroud)

用相同的方式编译时-O3。我不明白为什么a不管字符串长一字节,它仍不能识别出仍未使用。

这个问题与gcc 9.1和clang 8.0有关(在线:https : //gcc.godbolt.org/z/p1Z8Ns),因为在我看来,其他编译器要么完全删除未使用的变量(ellcc),要么为其生成代码,无论字符串的长度。

lub*_*bgr 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;
    };
   // ...
 };
Run Code Online (Sandbox Code Playgroud)

这样您就可以发现本地缓冲区的大小_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;
  }
Run Code Online (Sandbox Code Playgroud)

本地缓冲区中填充了其内容的位置。在这部分之后,我们到达耗尽本地容量的分支-分配了新的存储(通过中的allocate 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;
  }
Run Code Online (Sandbox Code Playgroud)

附带说明,小字符串优化本身就是一个话题。要了解调整单个位如何在很大程度上产生影响,我建议您进行本次演讲。它还提到(libstdc ++)std::string附带的实现是如何gcc工作的,并在过去进行了更改以匹配该标准的较新版本。

  • 实际上,Clang可以优化“ new”而不用担心底层实现。它在C ++ 14中已明确允许:请参见[分配部分](https://en.cppreference.com/w/cpp/language/new)“`delete [] new int [10];`可以进行优化出来”。 (11认同)
  • 请注意,最多16个字符是实现定义的。它适用于GCC / libstdc ++和MSVC和x86_64体系结构。Libc ++(通常与Clang一起使用)采用了另一种方法,并且限制更高(23个字符)。(Godbolt的Clang似乎根据生成的程序集使用libstdc ++。) (8认同)
  • ...而且我对编写编译器的人的尊重甚至更多。 (6认同)
  • 程序集输出中没有系统调用。 (4认同)
  • @DanielLangr:Godbolt已安装libc ++。要让clang使用它,请使用`-stdlib = libc ++`。是的,这确实允许clang8.0优化掉更长的字符串:https://gcc.godbolt.org/z/gVm_6R。Godbolt的clang安装类似于普通的GNU / Linux安装,默认情况下使用libstdc ++。 (4认同)

Pas*_* By 19

我很惊讶编译器看到了一个std::string构造函数/析构函数对,直到看到第二个示例为止。没有。您在此处看到的是小字符串优化以及编译器对此进行的相应优化。

小字符串优化是指std::string对象本身足够大以容纳字符串的内容,大小以及可能有区别的位,用于指示字符串是在小字符串模式还是大字符串模式下运行。在这种情况下,不会发生动态分配,并且字符串将存储在std::string对象本身中。

编译器在清除不必要的分配和释放方面确实很糟糕,它们几乎被视为具有副作用,因此无法排除。当您超过小的字符串优化阈值时,就会发生动态分配,结果就是您所看到的。

举个例子

void foo() {
    delete new int;
}
Run Code Online (Sandbox Code Playgroud)

是可能的最简单,最愚蠢的分配/取消分配对,但即使在O3下,gcc也会发出此程序集

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)
Run Code Online (Sandbox Code Playgroud)

  • 相关讨论:[是否允许编译器优化堆内存分配?](/sf/ask/2231153151/)。 (7认同)
  • Clang 3.8为我正确地优化了它(除非它是通过运算符new()函数调用来调用的),似乎这是一个gcc问题。 (5认同)
  • 使用了哪个编译器版本?据此:https://en.cppreference.com/w/cpp/language/new#Allocation,由于使用C ++ 14,因此可以优化此类分配。 (3认同)
  • *几乎像有副作用一样对待*该问题的部分原因可能是用户可以“替换” C ++的“ new”。因此,它确实*可能*具有副作用,例如日志记录分配。这也使得不可能将std :: vector的大小优化为realloc而不是new / copy / delete,除非编译器知道链接时没有替换过new,这确实是愚蠢的。标准中的C ++ 14保证可以优化`delete new ...`是有帮助的,但是并不是所有的编译器都在寻找它。 (3认同)