Der Compiler beendet die Optimierung nicht verwendeter Zeichenfolgen beim Hinzufügen von Zeichen

72

Ich bin gespannt warum der folgende Code:

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

Beim Kompilieren mit -O3ergibt sich folgender Code:

main:                                   # @main
    xor     eax, eax
    ret

(Ich verstehe vollkommen, dass das nicht verwendete nicht benötigt wird, aso dass der Compiler es vollständig aus dem generierten Code weglassen kann.)

Allerdings folgendes Programm:

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

Ausbeuten:

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"

wenn mit dem gleichen kompiliert -O3. Ich verstehe nicht, warum es nicht erkennt, adass der String noch nicht verwendet wird, unabhängig davon, dass der String ein Byte länger ist.

Diese Frage ist relevant für gcc 9.1 und clang 8.0 (online: https://gcc.godbolt.org/z/p1Z8Ns ), da andere Compiler in meiner Beobachtung entweder die nicht verwendete Variable (ellcc) entweder vollständig löschen oder Code dafür generieren, unabhängig von der Länge der Zeichenfolge.

Ferenc Deak
quelle
17
kann mit einigen Optimierungspraktiken für kurze Zeichenfolgen verbunden sein ?
UmNyobe
4
Könnte es an der Optimierung kleiner Zeichenfolgen liegen? Versuchen Sie, aals flüchtig zu deklarieren, und Sie sehen, dass die beiden Zeichenfolgen unterschiedlich behandelt werden. Das längste scheint auf dem Haufen zugeordnet zu sein. gcc.godbolt.org/z/WUuJIB
Davide Spataro
6
In diesem Thread wird erläutert, ob der Compiler dynamische Zuordnungen optimieren darf
MM
Wenn Sie string_viewstattdessen verwenden, wird immer noch eine längere Zeichenfolge entfernt optimiert: godbolt.org/z/AAViry
Ted Lyngmo
1
Versuchen Sie, -stdlib=libc++für die Zusammenstellung mit Clang anzuhängen
;-)

Antworten:

66

Dies ist auf die Optimierung kleiner Zeichenfolgen zurückzuführen. Wenn die Zeichenfolgendaten einschließlich des Nullterminators kleiner oder gleich 16 Zeichen sind, werden sie in einem Puffer gespeichert, der lokal für das std::stringObjekt selbst ist. Andernfalls wird Speicher auf dem Heap zugewiesen und die Daten dort gespeichert.

Die erste Zeichenfolge "ABCDEFGHIJKLMNO"plus der Nullterminator hat genau die Größe 16. Durch Hinzufügen "P"wird der Puffer überschritten, daher newwird er intern aufgerufen, was unweigerlich zu einem Systemaufruf führt. Der Compiler kann etwas wegoptimieren, wenn sichergestellt werden kann, dass keine Nebenwirkungen auftreten. Ein Systemaufruf macht dies wahrscheinlich unmöglich - durch Einschränkung ermöglicht das Ändern eines Puffers lokal für das im Bau befindliche Objekt eine solche Nebenwirkungsanalyse.

Das Verfolgen des lokalen Puffers in libstdc ++, Version 9.1, zeigt folgende Teile 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;
    };
   // ...
 };

Hiermit können Sie die lokale Puffergröße _S_local_capacityund den lokalen Puffer selbst erkennen ( _M_local_buf). Wenn der Konstruktor den basic_string::_M_constructAufruf auslöst , haben Sie 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;
  }

wo der lokale Puffer mit seinem Inhalt gefüllt ist. Unmittelbar nach diesem Teil gelangen wir zu dem Zweig, in dem die lokale Kapazität erschöpft ist - neuer Speicher wird zugewiesen (durch Zuweisen in M_create), der lokale Puffer wird in den neuen Speicher kopiert und mit dem Rest des Initialisierungsarguments gefüllt:

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

Nebenbei bemerkt, die Optimierung kleiner Zeichenfolgen ist ein eigenständiges Thema. Um ein Gefühl dafür zu bekommen, wie das Optimieren einzelner Bits im großen Maßstab einen Unterschied machen kann, würde ich diesen Vortrag empfehlen . Außerdem wird erwähnt, wie die std::stringmit gcc(libstdc ++) gelieferte Implementierung funktioniert und in der Vergangenheit geändert wurde, um sie an neuere Versionen des Standards anzupassen.

lubgr
quelle
4
Die Assembly-Ausgabe enthält keine Systemaufrufe.
Maxim Egorushkin
8
Beachten Sie, dass das Limit von 16 Zeichen implementierungsdefiniert ist. Es gilt für die GCC / libstdc ++ - und MSVC- und x86_64-Architektur. Libc ++ (normalerweise mit Clang verwendet) verwendet einen anderen Ansatz und das Limit ist dort höher (23 Zeichen). (Godbolts Clang verwendet anscheinend libstdc ++ gemäß der generierten Assembly.)
Daniel Langr
11
Tatsächlich kann Clang optimieren, newohne sich um die zugrunde liegende Implementierung kümmern zu müssen. In C ++ 14 ist dies ausdrücklich zulässig: siehe Abschnitt Zuordnung " delete[] new int[10];kann optimiert werden".
Matthieu M.
6
... und mein Respekt gegenüber Leuten, die Compiler schreiben, nimmt noch mehr zu.
Kedarps
4
@ DanielLangr: Godbolt hat libc ++ installiert. Verwenden Sie, um Clang verwenden zu lassen -stdlib=libc++. Und ja, dies ermöglicht es clang8.0, die längere Zeichenfolge zu optimieren: gcc.godbolt.org/z/gVm_6R . Die Clang-Installation von Godbolt ähnelt einer normalen GNU / Linux-Installation, bei der standardmäßig libstdc ++ verwendet wird.
Peter Cordes
19

Ich war überrascht, dass der Compiler ein std::stringKonstruktor / Destruktor-Paar durchgesehen hat, bis ich Ihr zweites Beispiel gesehen habe. Es war nicht so. Was Sie hier sehen, ist die Optimierung kleiner Zeichenfolgen und entsprechende Optimierungen durch den Compiler.

Kleine Zeichenfolgenoptimierungen liegen vor, wenn das std::stringObjekt selbst groß genug ist, um den Inhalt der Zeichenfolge, eine Größe und möglicherweise ein Unterscheidungsbit aufzunehmen, das angibt, ob die Zeichenfolge im Modus für kleine oder große Zeichenfolgen ausgeführt wird. In diesem Fall treten keine dynamischen Zuordnungen auf und die Zeichenfolge wird im std::stringObjekt selbst gespeichert .

Compiler sind wirklich schlecht darin, nicht benötigte Zuordnungen und Freigaben zu beseitigen. Sie werden fast so behandelt, als hätten sie Nebenwirkungen und sind daher unmöglich zu beseitigen. Wenn Sie den Schwellenwert für die Optimierung kleiner Zeichenfolgen überschreiten, treten dynamische Zuordnungen auf, und das Ergebnis wird angezeigt.

Als Beispiel

void foo() {
    delete new int;
}

ist das einfachste und dümmste mögliche Zuordnungs- / Freigabepaar, aber gcc gibt diese Baugruppe auch unter O3 aus

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)
Passant von
quelle
3
Welche Compilerversion wurde verwendet? Demnach: en.cppreference.com/w/cpp/language/new#Allocation , da C ++ 14 solche Zuordnungen optimieren darf.
Balázs Kovacsics
@ BalázsKovacsics gcc 9.1, Link zu Godbolt hinzugefügt.
Passant Bis zum
5
Clang 3.8 optimiert es korrekt für mich (es sei denn, es wird mit dem Funktionsaufruf operator new () aufgerufen), es scheint ein gcc-Problem zu sein.
Balázs Kovacsics
7
Relevante Diskussion: Darf der Compiler die Heap-Speicherzuordnungen optimieren? .
Daniel Langr
3
Es wird fast so behandelt, als hätte es Nebenwirkungen. Ein Teil dieses Problems könnte sein, dass C ++ newvom Benutzer "ersetzbar" ist. Es könnte also wirklich Nebenwirkungen haben, wie das Protokollieren von Zuordnungen. Dies macht es auch unmöglich, die std::vectorGrößenänderung reallocanstelle von new / copy / delete zu optimieren, es sei denn, der Compiler verfügt newüber nicht ersetzte Link-Time-Kenntnisse , was wirklich sehr, sehr dumm ist. Die C ++ 14-Garantie des Standards, delete new ...die optimiert werden kann, ist hilfreich, aber noch suchen nicht alle Compiler danach.
Peter Cordes
0

Während die akzeptierte Antwort gültig ist, da C ++ 14 , es ist tatsächlich der Fall , dass newund deleteAnrufe können wegoptimiert werden. Siehe diese arkane Formulierung auf cppreference:

Neue Ausdrücke dürfen ... Zuordnungen durch austauschbare Zuordnungsfunktionen entfernen. Im Falle einer Elision kann der Speicher vom Compiler bereitgestellt werden, ohne eine Zuweisungsfunktion aufzurufen (dies ermöglicht auch die Optimierung nicht verwendeter neuer Ausdrücke).

...

Beachten Sie, dass diese Optimierung nur zulässig ist, wenn neue Ausdrücke verwendet werden, keine anderen Methoden zum Aufrufen einer austauschbaren Zuordnungsfunktion: Sie delete[] new int[10];können optimiert werden, der Operator delete(operator new(10));jedoch nicht.

Auf diese Weise können Compiler Ihre lokale Datei vollständig löschen, std::stringselbst wenn sie sehr lang ist. Tatsächlich - clang ++ mit libc ++ tut dies bereits (GodBolt), da libc ++ integrierte Funktionen verwendet __newund __deletebei der Implementierung von std::string- ist dies "vom Compiler bereitgestellter Speicher". So erhalten wir:

main():
        xor eax, eax
        ret

mit im Grunde beliebig langen nicht verwendeten Zeichenfolgen.

GCC tut dies nicht, aber ich habe kürzlich Fehlerberichte darüber geöffnet. Links finden Sie in dieser SO-Antwort .

einpoklum
quelle
neue und gelöschte Ausdruckspaare, klar. Aufrufe zu op new und op delete, nicht ganz.
Deduplikator