Optimierung von raw new [] / delete [] vs. std :: vector

8

Machen wir uns mit sehr einfachem dynamisch zugewiesenem Speicher herum. Wir nehmen einen Vektor von 3, setzen seine Elemente und geben die Summe des Vektors zurück.

Im ersten Testfall habe ich einen rohen Zeiger mit new[] / delete[] verwendet. In der Sekunde habe ich std::vector :

benutzt %Vor%

Zusammenstellung von (1) ( new[] / delete[] )

%Vor%

Zusammenstellung von (2) ( std::vector )

%Vor%

Beide Ausgaben wurden aus Ссылка mit -std=c++14 -O3

übernommen

In beiden Versionen wird der zurückgegebene Wert zur Kompilierzeit berechnet, sodass wir nur mov eax, 6; ret sehen.

Mit der rohen new[] / delete[] wurde die dynamische Zuweisung komplett entfernt. Mit std::vector wird jedoch der Speicher zugewiesen, gesetzt und freigegeben.

Dies geschieht auch bei einer nicht verwendeten Variable auto v = std::vector<int>(3) : Aufruf an new , Speicher wird gesetzt und dann Aufruf von delete .

Ich weiß, dass dies wahrscheinlich eine fast unmögliche Antwort zu geben ist, aber vielleicht hat jemand einige Einsichten und einige interessante Antworten könnten herausspringen.

Was sind die Faktoren, die Compileroptimierungen nicht erlauben, die Speicherzuweisung im Fall std::vector zu entfernen, wie im Fall der Zuweisung von Speicherzuweisung?

    
bolov 04.01.2016, 12:12
quelle

2 Antworten

13

Bei Verwendung eines Zeigers zu einem dynamisch zugewiesenen Array (direkt mit new [] und delete []) hat der Compiler die Aufrufe an operator new und operator delete wegoptimiert, obwohl sie beobachtbare Nebenwirkungen haben. Diese Optimierung wird vom C ++ - Standardabschnitt 5.3.4 Absatz 10 ermöglicht:

  

Eine Implementierung darf einen Aufruf an ein ersetzbares globales Element auslassen   Zuweisungsfunktion (18.6.1.1, 18.6.1.2). Wenn dies der Fall ist, der Speicher   wird stattdessen von der Implementierung bereitgestellt oder ...

Ich werde den Rest des Satzes, der entscheidend ist, am Ende zeigen.

Diese Optimierung ist relativ neu, weil sie erstmals in C ++ 14 erlaubt war (Vorschlag N3664 ). Clang hat es seit 3.4 unterstützt . Die neueste Version von gcc, nämlich 5.3.0, nutzt diese Lockerung der Als-ob-Regel nicht aus. Es erzeugt den folgenden Code:

%Vor%

MSVC 2013 unterstützt diese Optimierung ebenfalls nicht. Es erzeugt den folgenden Code:

%Vor%

Ich habe derzeit keinen Zugriff auf MSVC 2015 Update 1 und daher weiß ich nicht, ob diese Optimierung unterstützt wird oder nicht.

Schließlich ist hier der von icc 13.0.1 generierte Assembly-Code:

%Vor%

Diese Optimierung wird nicht unterstützt. Ich habe keinen Zugriff auf die neueste Version von icc, nämlich 16.0.

Alle diese Code-Snippets wurden mit aktivierten Optimierungen erstellt.

Bei Verwendung von std::vector haben alle diese Compiler die Zuordnung nicht optimiert. Wenn ein Compiler keine Optimierung durchführt, liegt dies entweder daran, dass er aus irgendeinem Grund nicht möglich ist oder einfach nicht unterstützt wird.

  

Was sind die Faktoren, die Compiler nicht zulassen?   Optimierungen, um die Speicherzuordnung im Fall std :: vector zu entfernen,   wie im Fall der rohen Speicherzuweisung?

Der Compiler hat die Optimierung nicht durchgeführt, weil es nicht erlaubt ist. Sehen wir uns dazu den Rest des Satzes in Absatz 10 von 5.3.4 an:

  

Eine Implementierung darf einen Aufruf an ein ersetzbares globales Element auslassen   Zuweisungsfunktion (18.6.1.1, 18.6.1.2). Wenn dies der Fall ist, der Speicher   wird stattdessen durch die Implementierung bereitgestellt oder durch die Erweiterung der   Zuweisung von einem anderen neuen Ausdruck .

Dies besagt, dass Sie einen Aufruf an eine ersetzbare globale Zuweisungsfunktion nur dann weglassen können, wenn sie von einem neuen Ausdruck stammt. Ein neuer Ausdruck ist in Absatz 1 desselben Abschnitts definiert.

Der folgende Ausdruck

%Vor%

ist ein new-Ausdruck und daher darf der Compiler den zugehörigen Zuordnungsfunktionsaufruf optimieren.

Auf der anderen Seite der folgende Ausdruck:

%Vor%

ist KEIN neuer Ausdruck (siehe 5.3.4 Absatz 1). Dies ist nur ein Funktionsaufrufausdruck. Mit anderen Worten, dies wird als typischer Funktionsaufruf behandelt. Diese Funktion kann nicht entfernt werden, da sie aus einer anderen gemeinsam genutzten Bibliothek importiert wird (selbst wenn Sie die Laufzeit statisch verknüpft haben, ruft die Funktion selbst eine andere importierte Funktion auf).

Der von std::vector verwendete Standardzuordner weist Speicher zu, der ::operator new verwendet, und daher darf der Compiler ihn nicht optimieren.

Lass uns das testen. Hier ist der Code:

%Vor%

Wenn wir mit Clang 3.7 kompilieren, erhalten wir den folgenden Assemblierungscode:

%Vor%

Dies ist genau das gleiche wie Assemblercode, der bei Verwendung von std::vector generiert wurde, mit Ausnahme von mov qword ptr [rax], 0 , das vom Konstruktor von std :: vector stammt (der Compiler hätte es entfernen müssen, aber wegen eines Fehlers in seiner Optimierungsalgorithmen).

    
Hadi Brais 04.01.2016, 17:37
quelle
0

Es gibt ein paar Faktoren, die im Spiel sein können.

Zunächst ist die Qualität der Implementierung des Compilers, und wie tief es Code analysiert, wenn Funktionen zur Optimierung inlining sind. Diese Art der Analyse ist kompliziert - insbesondere in Verbindung mit einer Vorlagen-Maschinerie in einem C ++ - Compiler -, so dass möglicherweise ein erheblicher Entwickleraufwand in den Compiler implementiert werden muss. Ein solcher Code wird wahrscheinlich ziemlich kompliziert sein, wenn Code kompiliert wird (z. B. zunehmende Kompilierungszeiten, zunehmende Menge an Speicher oder andere Ressourcen, die von dem Kompilierer benötigt werden). Je komplizierter und kostspieliger es ist, etwas im Compiler zu implementieren, desto stärker muss es implementiert werden (z. B. wahrscheinlich für Entwickler in der realen Welt, nicht wahrscheinlich, dass Entwickler der realen Welt über übermäßige Kompilierungszeiten meckern, was nicht wahrscheinlich ist) um die Compiler-Entwickler dazu zu bringen, über die Implementierung dieser Fähigkeit im Compiler usw. zu meckern.)

Die dynamische Zuweisung mit dem Operator new und die Freigabe mit dem Operator delete sind Teil der Sprache. Wenn er in der gleichen Funktion verwendet wird, kann der Compiler dies sogar in einer frühen Parsing-Phase erkennen. Dinge können sich ändern, wenn Sie zum Beispiel die Anweisung new oder delete in eine separate Funktion einfügen. Wenn der Zeiger oder Zeiger auf Elemente oder [die Liste geht weiter] wird an eine andere Funktion übergeben. Aber in Ihrem Fall ist die Analyse ziemlich einfach.

Im Vergleich dazu ist std::vector Teil der Standardbibliothek. Nun, okay, es ist eine Vorlage, so dass die vollständige Definition für den Compiler sichtbar sein kann (dies hängt davon ab, wie die Bibliothek implementiert ist, sowie von Compiler- und Kompilierungsoptionen, was in der Praxis oft der Fall ist). Selbst wenn der Compiler vollständig sichtbar ist, muss er alle Operationen untersuchen, die den Vektor betreffen (Konstruktion, Verwendung seines Zuordners, Verhalten der Elementfunktionen wie operator[] , Destruktor usw.). Die Definition von std::vector und ihrer Elementfunktionen beinhaltet nun eine ganze Menge anderer Maschinen als einfache new und delete , und es würde einige ziemlich komplizierte Maschinen erfordern - oft in späteren Phasen der Kompilierung / Optimierung - um was zu analysieren dieser Code tut. Verweisen Sie meinen vorhergehenden Kommentar auf die Komplexität der Implementierung solcher Dinge in einem Compiler. Es gibt wahrscheinlich auch ein Argument, das gemacht werden könnte: Wenn jemand std::vector verwendet, sind sie wahrscheinlich weniger verwirrt, wenn der Compiler nicht alle beteiligten Maschinen optimiert, dh es muss der Fall sein, um die Komplexität bei der Implementierung des Compilers zu akzeptieren stärker und wahrscheinlich nicht.

Offensichtlich hängen die Besonderheiten von Argumenten wie dem obigen von der Arbeitsweise des Teams ab, das den Compiler und die Bibliothek erzeugt. Aber allgemein gesprochen ....

    
Peter 04.01.2016 13:01
quelle