c++ - Comportement incohérent de l'optimisation du compilateur de la chaîne inutilisée




gcc compilation (2)

Cela est dû à la petite optimisation de la chaîne. Lorsque la chaîne contient moins de 16 caractères, y compris le terminateur null, elle est stockée dans une mémoire tampon locale de l'objet std::string lui-même. Sinon, il alloue de la mémoire sur le tas et stocke les données là-bas.

La première chaîne "ABCDEFGHIJKLMNO" plus le terminateur nul est exactement de taille 16. L'ajout de "P" fait dépasser la mémoire tampon; par conséquent, la new chaîne est appelée en interne, ce qui conduit inévitablement à un appel système. Le compilateur peut optimiser quelque chose s'il est possible de s'assurer qu'il n'y a pas d'effets secondaires. Un appel système rend probablement cela impossible - par contraste, changer un tampon local de l'objet en construction permet une telle analyse des effets secondaires.

Le traçage du tampon local dans libstdc ++, version 9.1, révèle ces parties de 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;
    };
   // ...
 };

qui vous permet de repérer la taille du tampon local _S_local_capacity et le tampon local lui-même ( _M_local_buf ). Lorsque le constructeur déclenche l' basic_string::_M_construct , vous avez dans 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;
  }

où le tampon local est rempli avec son contenu. Juste après cette partie, nous arrivons à la succursale où la capacité locale est épuisée - un nouveau stockage est alloué (via M_create dans M_create ), le tampon local est copié dans le nouveau stockage et rempli avec le reste de l'argument d'initialisation:

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

En passant, l’optimisation des petites chaînes est un sujet à part entière. Je recommande cette discussion pour avoir une idée de la façon dont modifier des bits individuels peut faire une différence à grande échelle. Il mentionne également comment l’implémentation std::string livrée avec gcc (libstdc ++) fonctionne et a été modifiée au cours des précédentes pour correspondre aux nouvelles versions de la norme.

Je suis curieux de savoir pourquoi le code suivant:

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

lors de la compilation avec -O3 , le code suivant est obtenu:

main:                                   # @main
    xor     eax, eax
    ret

(Je comprends parfaitement qu'il n'y a pas besoin de ressources inutilisées pour que le compilateur puisse les omettre complètement du code généré)

Cependant le programme suivant:

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

rendements:

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"

lorsque compilé avec le même -O3 . Je ne comprends pas pourquoi il ne reconnaît pas que le a est toujours inutilisé, même si la chaîne a un octet de plus.

Cette question concerne gcc 9.1 et clang 8.0 (en ligne: https://gcc.godbolt.org/z/p1Z8Ns ) car d’autres compilateurs de mon observation abandonnent entièrement la variable inutilisée (ellcc) ou génèrent du code, quelle que soit la longueur de la chaîne.


J'ai été surpris que le compilateur ait scié un couple constructeur / destructeur std::string jusqu'à ce que je voie votre deuxième exemple. Ça n'a pas. Ce que vous voyez ici est une optimisation de petite chaîne et les optimisations correspondantes du compilateur autour de cela.

Les optimisations de petites chaînes surviennent lorsque l'objet std::string est lui-même suffisamment grand pour contenir le contenu de la chaîne, une taille et éventuellement un bit de discrimination utilisé pour indiquer si la chaîne fonctionne en mode petite ou grande chaîne. Dans un tel cas, aucune allocation dynamique ne se produit et la chaîne est stockée dans l'objet std::string lui-même.

Les compilateurs sont vraiment mauvais pour écarter les allocations et les désallocations inutiles, ils sont traités presque comme s'ils avaient des effets secondaires et sont donc impossibles à éliminer. Lorsque vous dépassez le seuil d'optimisation des petites chaînes, des allocations dynamiques se produisent et le résultat obtenu est celui que vous voyez.

Par exemple

void foo() {
    delete new int;
}

est la paire d'allocation / désallocation la plus simple et la plus stupide possible, et pourtant gcc émet cet assemblage même sous 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)




compiler-optimization