1. ホーム
  2. c++

コンパイラは、文字を追加する際に未使用の文字列を最適化しなくなる

2023-11-22 18:48:59

質問

私は、なぜ次のコードの部分があるのかが気になります。

#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 は、文字列が 1 バイト長くなっても、まだ使われていないことを認識しない理由が理解できます。

この質問は、gcc 9.1 と clang 8.0 に関連するものです(オンライン。 https://gcc.godbolt.org/z/p1Z8Ns 私の観察では、他のコンパイラーは未使用の変数 (ellcc) を完全に削除するか、文字列の長さに関係なくそのためのコードを生成するからです。

どのように解決するのですか?

これは、文字列が小さい場合の最適化に起因します。文字列データがヌルターミネータを含めて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;
  }

余談ですが、小さな文字列の最適化は、それだけでかなりのトピックです。個々のビットを微調整することで、大規模な違いを生み出すことができることを実感するには このトーク . また、この講演では std::string に同梱されている実装が gcc (libstdc++) が動作し、より新しいバージョンの規格に適合するように過去に変更されました。