Compiler stops optimizing unused string away when adding characters

前端 未结 3 1931
日久生厌
日久生厌 2020-12-23 18:49

I am curious why the following piece of code:

#include 
int main()
{
    std::string a = \"ABCDEFGHIJKLMNO\";
}

when compiled

相关标签:
3条回答
  • 2020-12-23 19:24

    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.

    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.

    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;
        };
       // ...
     };
    

    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;
      }
    

    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;
      }
    

    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.

    0 讨论(0)
  • 2020-12-23 19:25

    While the accepted answer is valid, since C++14 it's actually the case that new and delete calls can be optimized away. See this arcane wording on cppreference:

    New-expressions are allowed to elide ... allocations made through replaceable allocation functions. In case of elision, the storage may be provided by the compiler without making the call to an allocation function (this also permits optimizing out unused new-expression).

    ...

    Note that this optimization is only permitted when new-expressions are used, not any other methods to call a replaceable allocation function: delete[] new int[10]; can be optimized out, but operator delete(operator new(10)); cannot.

    This actually allows compilers to completely drop your local std::string even if it's very long. In fact - clang++ with libc++ already does this (GodBolt), since libc++ uses built-ins __new and __delete in its implementation of std::string - that's "storage provided by the compiler". Thus, we get:

    main():
            xor eax, eax
            ret
    

    with basically any-length unused string.

    GCC doesn't do but I've recently opened bug reports about this; see this SO answer for links.

    0 讨论(0)
  • 2020-12-23 19:34

    I was surprised the compiler saw through a std::string constructor/destructor pair until I saw your second example. It didn't. What you're seeing here is small string optimization and corresponding optimizations from the compiler around that.

    Small string optimizations are when the std::string object itself is big enough to hold the contents of the string, a size and possibly a discriminating bit used to indicate whether the string is operating in small or big string mode. In such a case, no dynamic allocations occur and the string is stored in the std::string object itself.

    Compilers are really bad at eliding unneeded allocations and deallocations, they are treated almost as if having side effects and are thus impossible to elide. When you go over the small string optimization threshold, dynamic allocations occur and the result is what you see.

    As an example

    void foo() {
        delete new int;
    }
    

    is the simplest, dumbest allocation/deallocation pair possible, yet gcc emits this assembly even under O3

    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)
    
    0 讨论(0)
提交回复
热议问题