Kompilator przestaje optymalizować nieużywany ciąg znaków podczas dodawania znaków

Jestem ciekaw dlaczego następujący fragment kodu:

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

Po skompilowaniu z -O3 daje następujący kod:

main:                                   # @main
    xor     eax, eax
    ret

(doskonale rozumiem, że nie ma potrzeby używania nieużywanego a, więc kompilator może całkowicie pominąć go z wygenerowanego kodu)

Jednak następujący program:

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

Wydajność:

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"

W przypadku kompilacji z tym samym -O3. Nie rozumiem, dlaczego nie rozpoznaje, że a jest nadal nieużywany, niezależnie że ciąg jest o jeden bajt dłuższy.

To pytanie dotyczy gcc 9.1 i clang 8.0, (online: https://gcc.godbolt.org/z/p1Z8Ns ) ponieważ inne Kompilatory z mojej obserwacji albo całkowicie upuszczają nieużywaną zmienną (ellcc) albo generują dla niej kod niezależnie od długości łańcucha.

Author: einpoklum, 2019-06-03

3 answers

Wynika to z małej optymalizacji ciągu. Gdy dane ciągu znaków są mniejsze lub równe 16 znakom, łącznie z terminatorem null, są przechowywane w buforze lokalnym do samego obiektu std::string. W przeciwnym razie przydziela pamięć na stercie i przechowuje tam dane.

Pierwszy ciąg "ABCDEFGHIJKLMNO" plus null terminator ma dokładnie rozmiar 16. Dodanie {[5] } powoduje przekroczenie bufora, stąd {[6] } jest wywoływane wewnętrznie, co nieuchronnie prowadzi do wywołania systemowego. Kompilator może zoptymalizuj coś, jeśli jest to możliwe, aby zapewnić, że nie ma skutków ubocznych. Wywołanie systemowe prawdopodobnie uniemożliwia zrobienie tego-przez constrast, zmiana bufora lokalnego na obiekt w budowie pozwala na taką analizę efektu ubocznego.

Śledzenie lokalnego bufora w libstdc++, Wersja 9.1, ujawnia te części 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;
    };
   // ...
 };

, który pozwala dostrzec rozmiar lokalnego bufora _S_local_capacity oraz sam lokalny bufor (_M_local_buf). Gdy konstruktor uruchamia basic_string::_M_construct wywołanie, masz w 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;
  }

Gdzie bufor lokalny jest wypełniony jego zawartością. Zaraz po tej części, dostajemy się do gałęzi, gdzie lokalna pojemność jest wyczerpana - przydzielana jest nowa pamięć masowa (poprzez alokację w M_create), lokalny bufor jest kopiowany do nowej pamięci masowej i wypełniany resztą argumentu inicjalizującego:

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

Na marginesie, optymalizacja małych ciągów to całkiem sam temat. Aby poczuć, że jak Podkręcanie poszczególnych bitów może zrobić różnicę na dużą skalę, polecam tę rozmowę. Wspomina również, jak działa implementacja std::string dostarczana z gcc (libstdc++) i zmieniana w przeszłości, aby pasowała do nowszych wersji standardu.

 66
Author: lubgr,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-06-04 08:38:59

Byłem zaskoczony, że kompilator widział parę std::string konstruktor/Destruktor, dopóki nie zobaczyłem twojego drugiego przykładu. To, co widzisz tutaj, to mała optymalizacja ciągów i odpowiednie optymalizacje z kompilatora wokół tego.

Optymalizacja małych łańcuchów ma miejsce wtedy, gdy obiekt std::string jest wystarczająco duży, aby pomieścić zawartość łańcucha, rozmiar i ewentualnie bit rozróżniający używany do wskazania, czy łańcuch działa w trybie małym czy dużym łańcuchu. W takim case, nie występują dynamiczne przydziały, a łańcuch jest przechowywany w samym obiekcie std::string.

Kompilatory są naprawdę złe w unikaniu niepotrzebnych przydziałów i dealokacji, są traktowane prawie tak, jakby miały skutki uboczne i dlatego są niemożliwe do uniknięcia. Gdy przekroczysz próg optymalizacji małych ciągów, pojawią się dynamiczne alokacje, a wynik jest tym, co widzisz.

Jako przykład

void foo() {
    delete new int;
}

Jest najprostszą, najgłupszą możliwą parą alokacji/dealokacji, ale gcc emituje ten zespół nawet pod 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)
 19
Author: Passer By,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-06-03 10:43:27

Chociaż akceptowana odpowiedź jest prawidłowa, to od C++14 tak naprawdę jest tak, że wywołania new i delete mogą być zoptymalizowane. Zobacz to tajemne sformułowanie na cppreference:

Nowe-wyrażenia mogą znikać ... alokacje dokonywane za pomocą wymiennych funkcji alokacji. W przypadku usunięcia, pamięć może być dostarczona przez kompilator bez wywoływania funkcji alokacji (pozwala to również na optymalizację nieużywanego new-expression).

...

Zauważ, że ta optymalizacja jest dozwolona tylko wtedy, gdy new-expressions są używane, a nie Inne metody wywołania funkcji alokacji wymiennej: delete[] new int[10]; można zoptymalizować, ale operator Nie mogę.

To pozwala kompilatorom całkowicie porzucić lokalny std::string, nawet jeśli jest bardzo długi. W rzeczywistości-clang++ z libc++ robi to już (GodBolt), ponieważ libc++ używa wbudowanych __new i __delete w swojej implementacji std::string - to "storage provided by the compiler". W ten sposób otrzymujemy:

main():
        xor eax, eax
        ret

Z nieużywanym ciągiem o dowolnej długości.

GCC nie działa, ale niedawno otworzyłem raporty o błędach na ten temat; zobacz to więc odpowiedz na linki.

 0
Author: einpoklum,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-03-23 23:05:06