Ist Optimierungsstufe -O3 in g ++ gefährlich?

232

Ich habe aus verschiedenen Quellen gehört (obwohl meistens von einem Kollegen von mir), dass das Kompilieren mit einer Optimierungsstufe von -O3in g ++ irgendwie "gefährlich" ist und generell vermieden werden sollte, es sei denn, dies hat sich als notwendig erwiesen.

Ist das wahr und wenn ja, warum? Soll ich mich nur daran halten -O2?

Dunnie
quelle
38
Es ist nur gefährlich, wenn Sie sich auf undefiniertes Verhalten verlassen. Und selbst dann wäre ich überrascht, wenn es die Optimierungsstufe wäre, die etwas durcheinander gebracht hätte.
Seth Carnegie
5
Der Compiler ist weiterhin gezwungen, ein Programm zu erstellen, das sich so verhält, als ob es Ihren Code genau kompiliert hätte. Ich weiß nicht, dass -O3das als besonders fehlerhaft gilt? Ich denke, vielleicht kann es undefiniertes Verhalten "schlimmer" machen, da es seltsame und wundervolle Dinge tun kann, die auf bestimmten Annahmen beruhen, aber das wäre Ihre eigene Schuld. Im Allgemeinen würde ich sagen, dass es in Ordnung ist.
BoBTFish
5
Es ist richtig, dass höhere Optimierungsstufen anfälliger für Compiler-Fehler sind. Ich habe selbst einige Fälle getroffen, aber im Allgemeinen sind sie immer noch ziemlich selten.
Mysticial
21
-O2schaltet sich ein -fstrict-aliasing, und wenn Ihr Code das überlebt, überlebt er wahrscheinlich andere Optimierungen, da die Leute immer wieder falsch liegen. Das heißt, -fpredictive-commoningist nur in -O3und das Aktivieren kann Fehler in Ihrem Code ermöglichen, die durch falsche Annahmen über die Parallelität verursacht werden. Je weniger falsch Ihr Code ist,
Steve Jessop
6
@PlasmaHH, ich denke nicht, dass "strenger" eine gute Beschreibung von ist -Ofast, es deaktiviert zum Beispiel die IEEE-konforme Handhabung von NaNs
Jonathan Wakely

Antworten:

223

In den frühen Tagen von gcc (2.8 usw.) und in den Zeiten von egcs und redhat 2.96 -O3 war es manchmal ziemlich fehlerhaft. Aber das ist über ein Jahrzehnt her und -O3 unterscheidet sich nicht wesentlich von anderen Optimierungsstufen (in Buggyness).

Es werden jedoch tendenziell Fälle aufgedeckt, in denen sich Menschen auf undefiniertes Verhalten verlassen, da sie sich strenger auf die Regeln und insbesondere Eckfälle der Sprache (n) stützen.

Persönlich betreibe ich seit vielen Jahren Produktionssoftware im Finanzsektor mit -O3 und bin noch nicht auf einen Fehler gestoßen, der nicht vorhanden gewesen wäre, wenn ich -O2 verwendet hätte.

Auf vielfachen Wunsch hier eine Ergänzung:

-O3 und insbesondere zusätzliche Flags wie -funroll-Schleifen (nicht von -O3 aktiviert) können manchmal dazu führen, dass mehr Maschinencode generiert wird. Unter bestimmten Umständen (z. B. auf einer CPU mit außergewöhnlich kleinem L1-Befehls-Cache) kann dies zu einer Verlangsamung führen, da der gesamte Code von z. B. einer inneren Schleife jetzt nicht mehr in L1I passt. Im Allgemeinen ist gcc sehr bemüht, nicht so viel Code zu generieren. Da dies jedoch normalerweise den generischen Fall optimiert, kann dies passieren. Besonders anfällige Optionen (wie das Abrollen von Schleifen) sind normalerweise nicht in -O3 enthalten und werden in der Manpage entsprechend gekennzeichnet. Daher ist es im Allgemeinen eine gute Idee, -O3 zum Generieren von schnellem Code zu verwenden und nur dann auf -O2 oder -Os (das versucht, die Codegröße zu optimieren) zurückzugreifen, wenn dies angemessen ist (z. B. wenn ein Profiler L1I-Fehler anzeigt).

Wenn Sie die Optimierung auf das Äußerste bringen möchten, können Sie gcc über --param anpassen, um die mit bestimmten Optimierungen verbundenen Kosten zu ermitteln. Beachten Sie außerdem, dass gcc jetzt die Möglichkeit hat, Funktionen, die die Optimierungseinstellungen nur für diese Funktionen steuern, Attribute zuzuweisen. Wenn Sie also ein Problem mit -O3 in einer Funktion haben (oder spezielle Flags nur für diese Funktion ausprobieren möchten), Sie müssen nicht die gesamte Datei oder das gesamte Projekt mit O2 kompilieren.

otoh es scheint, dass bei der Verwendung von -Ofast Vorsicht geboten ist, in der es heißt:

-Ofast aktiviert alle -O3-Optimierungen. Es ermöglicht auch Optimierungen, die nicht für alle standardkonformen Programme gültig sind.

was mich zu dem Schluss bringt, dass -O3 vollständig standardkonform sein soll.

PlasmaHH
quelle
2
Ich benutze einfach so etwas wie das Gegenteil. Ich verwende immer -Os oder -O2 (manchmal generiert O2 eine kleinere ausführbare Datei). Nach der Profilerstellung verwende ich O3 für Teile des Codes, die mehr Ausführungszeit benötigen und die allein bis zu 20% mehr Geschwindigkeit bringen können.
CoffeDeveloper
3
Ich mache das aus Gründen der Geschwindigkeit. O3 macht die Dinge meistens langsamer. Ich weiß nicht genau warum, ich vermute, es verschmutzt den Anweisungscache.
CoffeDeveloper
4
@DarioOO Ich habe das Gefühl, dass "Code Bloat" eine beliebte Sache ist, aber ich sehe es fast nie mit Benchmarks. Es hängt stark von der Architektur ab, aber jedes Mal, wenn ich veröffentlichte Benchmarks sehe (z. B. phoronix.com/… ), zeigt sich, dass O3 in den allermeisten Fällen schneller ist. Ich habe die Profilerstellung und sorgfältige Analyse gesehen, die erforderlich sind, um zu beweisen, dass das Aufblähen von Code tatsächlich ein Problem war, und dies geschieht normalerweise nur bei Personen, die Vorlagen auf extreme Weise akzeptieren.
Nir Friedman
1
@NirFriedman: Es tritt häufig ein Problem auf, wenn das Inlining-Kostenmodell des Compilers Fehler aufweist oder wenn Sie für ein völlig anderes Ziel optimieren, als Sie ausführen. Interessanterweise gilt dies für alle Optimierungsstufen ...
PlasmaHH
1
@PlasmaHH: Das using-cmov-Problem ist für den allgemeinen Fall schwer zu beheben. Normalerweise haben Sie Ihre Daten nicht nur sortiert. Wenn gcc also versucht, zu entscheiden, ob ein Zweig vorhersehbar ist oder nicht, std::sorthilft eine statische Analyse, die nach Aufrufen von Funktionen sucht, wahrscheinlich nicht weiter. Die Verwendung von stackoverflow.com/questions/109710/… würde helfen, oder Sie könnten die Quelle schreiben, um die Sortierung zu nutzen: Scannen Sie, bis Sie> = 128 sehen, und beginnen Sie dann mit der Summierung. Was den aufgeblähten Code betrifft, habe ich vor, ihn zu melden. : P
Peter Cordes
42

Nach meiner etwas wechselhaften Erfahrung wird das Anwenden -O3auf ein gesamtes Programm fast immer langsamer (im Vergleich zu -O2), da es das Abrollen und Inlining aggressiver Schleifen aktiviert, wodurch das Programm nicht mehr in den Anweisungscache passt. Bei größeren Programmen kann dies auch -O2relativ zu -Os!

Das Verwendungsmuster für -O3besteht darin, dass Sie Ihr Programm nach dem Profilieren manuell auf eine kleine Handvoll Dateien anwenden, die kritische innere Schleifen enthalten, die tatsächlich von diesen aggressiven Kompromissen zwischen Platz und Geschwindigkeit profitieren. Neuere Versionen von GCC verfügen über einen profilgesteuerten Optimierungsmodus, mit dem (IIUC) die -O3Optimierungen selektiv auf Hot-Funktionen angewendet werden können, wodurch dieser Prozess effektiv automatisiert wird.

zwol
quelle
10
"fast immer"? Machen Sie es "50-50", und wir werden einen Deal haben ;-).
No-Bugs Hare
12

Die Option -O3 aktiviert zusätzlich zu allen Optimierungen der unteren Ebenen '-O2' und '-O1' teurere Optimierungen wie Funktionsinlining. Die Optimierungsstufe '-O3' kann die Geschwindigkeit der resultierenden ausführbaren Datei erhöhen, aber auch ihre Größe erhöhen. Unter bestimmten Umständen, wenn diese Optimierungen nicht günstig sind, kann diese Option ein Programm tatsächlich verlangsamen.

neel
quelle
3
Ich verstehe, dass einige "offensichtliche Optimierungen" ein Programm verlangsamen könnten, aber haben Sie eine Quelle, die behauptet, dass GCC-O3 ein Programm langsamer gemacht hat?
Mooing Duck
1
@MooingDuck: Obwohl ich keine Quelle zitieren kann, erinnere ich mich an einen solchen Fall mit einigen älteren AMD-Prozessoren, die einen ziemlich kleinen L1I-Cache hatten (~ 10k Anweisungen). Ich bin mir sicher, dass Google mehr für Interessierte bietet, aber insbesondere Optionen wie das Abrollen von Schleifen sind nicht Teil von O3, und diese erhöhen die Größe erheblich. -Os ist diejenige, wenn Sie die ausführbare Datei klein machen möchten. Sogar -O2 kann die Codegröße erhöhen. Ein gutes Werkzeug, um mit dem Ergebnis verschiedener Optimierungsstufen zu spielen, ist der gcc-Explorer.
PlasmaHH
@PlasmaHH: Eigentlich ist eine winzige Cache-Größe etwas, was ein Compiler vermasseln könnte, ein guter Punkt. Das ist ein wirklich gutes Beispiel. Bitte geben Sie es in die Antwort ein.
Mooing Duck
1
@PlasmaHH Pentium III hatte 16 KB Code-Cache. AMDs K6 und höher hatten tatsächlich einen 32-KB-Anweisungscache. P4 begann mit einem Wert von rund 96 KB. Core I7 verfügt tatsächlich über einen 32 KB L1-Code-Cache. Befehlsdecoder sind heutzutage stark, sodass Ihr L3 für fast jede Schleife geeignet ist.
Doug65536
1
Sie werden jedes Mal eine enorme Leistungssteigerung feststellen, wenn eine Funktion in einer Schleife aufgerufen wird, und sie kann eine erhebliche gemeinsame Eliminierung von Unterausdrücken bewirken und unnötige Neuberechnungen aus der Funktion vor der Schleife herausheben.
Doug65536
8

Ja, O3 ist fehlerhafter. Ich bin ein Compiler-Entwickler und habe eindeutige und offensichtliche GCC-Fehler identifiziert, die durch die Erstellung fehlerhafter SIMD-Montageanweisungen durch O3 beim Erstellen meiner eigenen Software verursacht wurden. Soweit ich gesehen habe, werden die meisten Produktionssoftware mit O2 ausgeliefert, was bedeutet, dass O3 beim Testen und bei der Behebung von Fehlern weniger Aufmerksamkeit erhält.

Stellen Sie sich das so vor: O3 fügt mehr Transformationen zu O2 hinzu, wodurch mehr Transformationen zu O1 hinzugefügt werden. Statistisch gesehen bedeuten mehr Transformationen mehr Fehler. Das gilt für jeden Compiler.

David Yeager
quelle
3

Vor kurzem hatte ich ein Problem mit der Optimierung mit g++. Das Problem hing mit einer PCI-Karte zusammen, bei der die Register (für Befehle und Daten) durch eine Speicheradresse dargestellt wurden. Mein Treiber hat die physische Adresse einem Zeiger in der Anwendung zugeordnet und sie dem aufgerufenen Prozess übergeben, der wie folgt damit funktioniert hat:

unsigned int * pciMemory;
askDriverForMapping( & pciMemory );
...
pciMemory[ 0 ] = someCommandIdx;
pciMemory[ 0 ] = someCommandLength;
for ( int i = 0; i < sizeof( someCommand ); i++ )
    pciMemory[ 0 ] = someCommand[ i ];

Die Karte hat nicht wie erwartet funktioniert. Als ich die Versammlung sah verstand ich , dass der Compiler nur schrieb someCommand[ the last ]in pciMemoryalle vorhergehenden Schreibvorgänge weggelassen.

Fazit: Seien Sie genau und aufmerksam bei der Optimierung.

borisbn
quelle
38
Aber der Punkt hier ist, dass Ihr Programm einfach undefiniertes Verhalten hat; Der Optimierer hat nichts falsch gemacht. Insbesondere müssen Sie pciMemoryals deklarieren volatile.
Konrad Rudolph
11
Es ist eigentlich nicht UB, aber der Compiler hat das Recht, alle bis auf die letzten Schreibvorgänge wegzulassen, pciMemoryda alle anderen Schreibvorgänge nachweislich keine Wirkung haben. Für den Optimierer ist das großartig, weil er viele nutzlose und zeitaufwändige Anweisungen entfernen kann.
Konrad Rudolph
4
Ich fand dies im Standard (nach mehr als 10 Jahren))) - Eine flüchtige Deklaration kann verwendet werden, um ein Objekt zu beschreiben, das einem speicherabgebildeten Eingabe- / Ausgabeport oder einem Objekt entspricht, auf das über eine asynchron unterbrechende Funktion zugegriffen wird. Aktionen auf so deklarierte Objekte dürfen durch eine Implementierung nicht "optimiert" oder neu angeordnet werden, es sei denn, dies ist nach den Regeln für die Bewertung von Ausdrücken zulässig.
Borisbn
2
@borisbn Etwas abseits des Themas, aber woher wissen Sie, dass Ihr Gerät den Befehl übernommen hat, bevor Sie einen neuen Befehl senden?
user877329
3
@ user877329 Ich sah es durch das Verhalten des Geräts, aber es war eine großartige Suche
Borisbn