添加字符时,编译器停止优化未使用的字符串


72

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

#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)要么为其生成代码,无论字符串的长度。


17
可能链接到一些短字符串优化实践
UmNyobe

4
可能是由于小的字符串优化?尝试声明a为volatile,您会看到两个字符串的处理方式有所不同。最长的似乎是在堆上分配的。gcc.godbolt.org/z/WUuJIB
Davide Spataro

6
有关是否允许编译器优化动态分配的讨论,请参见此线程的讨论
MM

我用的是string_view它,它仍然可以优化更长的字符串:godbolt.org/z/AAViry
Ted

1
尝试附加-stdlib=libc++以Clang进行编译;-)
Daniel Langr

Answers:


66

这是由于小的字符串优化。当字符串数据少于或等于16个字符(包括空终止符)时,它将存储在std::string对象本身本地的缓冲区中。否则,它将在堆上分配内存并将数据存储在堆上。

第一个字符串"ABCDEFGHIJKLMNO"加上null终止符的大小恰好为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;
  }

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

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


4
程序集输出中没有系统调用。
Maxim Egorushkin

8
请注意,最多16个字符是实现定义的。它适用于GCC / libstdc ++和MSVC和x86_64体系结构。Libc ++(通常与Clang一起使用)采用了另一种方法,并且限制更高(23个字符)。(Godbolt的Clang似乎根据生成的程序集使用libstdc ++。)
Daniel Langr

11
实际上,Clang可以优化而new不用担心底层实现。在C ++ 14中明确允许:请参见“分配部分”中的“delete[] new int[10];可以优化”。
Matthieu M.

6
...而且我对编写编译器的人的尊重甚至更多。
kedarps

4
@DanielLangr:Godbolt已安装libc ++。要使用clang,请使用-stdlib=libc++。是的,这确实允许clang8.0优化掉更长的字符串:gcc.godbolt.org/z/gVm_6R。Godbolt的clang安装就像普通的GNU / Linux安装一样,默认情况下使用libstdc ++。
彼得·科德斯

19

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

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

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

举个例子

void foo() {
    delete new int;
}

是最简单,最愚蠢的分配/取消分配对,但即使在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)

3
使用了哪个编译器版本?据此:en.cppreference.com/w/cpp/language/new#Allocation,由于使用C ++ 14,因此可以优化此类分配。
巴拉兹Kovacsics

@BalázsKovacsicsgcc 9.1,添加了指向godbolt的链接。
路人

5
Clang 3.8为我正确地对其进行了优化(除非它是通过运算符new()函数调用来调用的),似乎这是一个gcc问题。
巴拉兹Kovacsics


3
几乎被视为具有副作用。此问题的部分原因可能是new用户可以“替换” C ++ 。因此,它实际上可能会产生副作用,例如记录分配。这也使得不可能优化std::vector调整大小,realloc而不是优化new / copy / delete,除非编译器new具有尚未被替换的链接时知识,这确实是愚蠢的。delete new ...可以优化的标准中的C ++ 14保证是有帮助的,但并非所有编译器都在寻找它。
彼得·科德斯

0

虽然接受的答案是有效的,因为C ++ 14它实际上的情况是newdelete调用可以被优化掉。请参阅有关cppreference的奥秘字眼:

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

...

请注意,仅当使用new-expressions时才允许进行此优化,而不能使用任何其他方法来调用可替换的分配函数: delete[] new int[10];可以进行优化,而运算符 delete(operator new(10));则不能。

实际上,这使编译器可以完全删除本地,std::string即使它很长也是如此。实际上-带有libc ++的clang ++已经做到了这一点(GodBolt),因为libc ++使用内置函数__new并且__delete在其实现中std::string-这就是“编译器提供的存储”。因此,我们得到:

main():
        xor eax, eax
        ret

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

GCC没有,但是我最近打开了有关此问题的错误报告。看到这个SO答案的链接。


确定是new和delete表达式对。调用op new和op delete的调用并不完全。
Deduplicator
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.