Dieser Text ist keine Dokumentation oder umfassende Anleitung zur Benutzung von C- oder C++-Compilern, sondern eine Zusammenstellung der in meinen Augen wichtigsten Compileroptionen, inklusive einiger Empfehlungen zur Benutzung.
Das grundsätzliche Optimierungsverhalten wird durch zwei Einstellungen beeinflusst: Erstens durch die Aktivierung von Linkzeitoptimierungen (Linkzeit-Codegenerierung, LTCG)
bzw. profilergestützter Optimierung (PGO). PGO gilt gewissermaßen als die Wunderwaffe unter den Optimierungen, weil die Optimierungsheuristiken dann auf Basis von Laufzeitmessungen
erfolgen. Weiß der Optimierer, wo er ansetzen muss, anstatt im Nebel zu stochern, wird das Ergebnis naturgemäß besser. Das Verfahren ist allerdings recht aufwändig, weil der
Entwickler eben für diese Daten einer Laufzeitmessung sorgen muss. Es ist also ein zweischrittiges Verfahren; im ersten Schritt wird untersucht, welche Codeteile
des unoptimierten Programms welche Relevanz für die Programmgeschwindigkeit haben, was dann im zweiten Schritt die Grundlage für die Optimierung ist. PGO führt in der Regel zu
deutlichen schnelleren und kleineren Programmen. Allerdings ist die Laufzeitmessung sehr zeitaufwändig, sodass dieses Verfahren nur für Endbenutzer-Builds angebracht scheint.
Linkzeit-Codegenerierung ist gewissermaßen der kleine Bruder der PGO. Sie dient dazu, die Hürde zu überwinden, dass normalerweise keine Optimierungen über Modulgrenzen hinweg stattfinden
können (etwa „Inlining“ eines Funktionsaufrufs in einer Datei, die in einer anderen .c/.cpp-Datei implementiert wurde). Diese Hürde wird durch einen Optimierungslauf im Linker
überwunden, setzt aber die Generierung eines dafür geeigneten Objektcodes durch den Compiler voraus. Insofern hilft LTCG nur, wenn Optimierungspotential über verschiedene Quelldateien
hinweg besteht. Erfahrungsgemäß kann sie die Compilezeiten, sowie im Einzelfall auch Programmgröße und sogar die Laufzeiten deutlich verschlechtern; im Zweifel sind die Auswirkungen im Einzelfall auszuprobieren.
Eingeschaltet wird LTCG über den Linkerschalter /ltcg
und den Compilerschalter /GL
(beide müssen gesetzt sein). In Visual Studio sorgt die Einstellung
„Link-Zeitcodegenerierung“ unter „Optimierung des gesamten Programms“ im Unterpunkt „Allgemein“ dafür, dass beide Schalter gesetzt werden.
Dort kann auch PGO aktiviert werden.
Die zweite zentrale Einstellung heißt „Optimierung“ unter „C/C++“ → „Optimierung“. Dort besteht die Wahl zwischen abgeschalteter
Optimierung (/Od
), Optimierung auf Programmgröße (/O1
) und Optimierung auf Geschwindigkeit (/O2
). Tatsächlich ist /O2
eine
Steigerung von /O1
, die dann auch Optimierungen einschließt, die die Programmgröße erhöhen, einschließt. Die Option „Vollständige Optimierung“
(/Ox
) aktiviert jedoch weniger Optimierungen als /O2
. In der Regel dürfte /O2
das Mittel der Wahl sein. Diese Option aktiviert
standardmäßig auch die meisten der darunter aufgeführten Optimierungen; will man jedoch sicher gehen, kann man diese auch manuell aktivieren (oder deaktivieren!). Fiber-sichere
Optimierungen sind bei Programmen, die kein Multithreading nutzen, jedoch nicht sinnvoll, und „Optimierung des gesammten Programms“ (/GL
) ist nur in
Verbindung mit LTCG (s.o.) von Bedeutung und führt sonst vor allem nur zu erheblich größeren Binärdateien.
Daneben gibt es noch ein paar andere Optionen, die zu Optimierungen herangezogen werden können: Zum einen „Stringpooling“ unter „C/C++“ →
„Codegenerierung“, einer nützlichen Optimierung der Programmgröße, die bei /O1
und /O2
allerdings schon eingeschaltet ist. Weiterhin befindet
sich dort die Option „Sicherheitsüberprüfungen“ (/GS
). Deaktiviert man diese (/GS-
), erhält man unter Umständen schnelleren und kleineren
Code, weil der Stack nicht mehr überwacht wird; bei sicherheitsrelevanter Software ist davon allerdings dringend abzuraten. Nicht in der GUI aufgeführt werden
die Optionen /Zc:inline
(seit Visual Studio 2015 unter „C/C++“ → „Sprache“ zu finden, und seitdem auch standardmäßig aktiv), die eine
aus Kompatibilitätsgründen in älteren Versionen nicht durchgeführte Entfernung ungenutzer Objekte zum frühestmöglichen Zeitpunkt aktiviert. Das neue Verhalten ist jedoch
standardkonform und die Option sollte daher in jedem Fall aktiviert werden. Außerdem existiert die Option /Zc:throwingNew
, die ebenfalls mit einer Altlast aufräumt;
Sehr alte MSVC-Versionen hatten Implementationen von operator new
, die 0 zurückgaben, anstatt eine Ausnahme zu werfen. Auch diese Option sollte aktiviert werden,
außer der Code erwartet, dass new
0 zurückgibt. Die Binärdatei verkleinern kann man zudem noch durch Abschaltung der Laufzeittypinfo (zu finden unter
„C/C++“ → „Sprache“). Sofern der Code keinen Gebrauch von dynamic_cast
oder typeid()
macht, sollte dies keine
Beeinträchtigungen nach sich ziehen. Zuletzt existieren noch zwei Optimierungen unter „Linker“ → „Optimierung“, nämlich /OPT:REF
(„Verweise“) und /OPT:ICF
(„COMDAT-Faltung“), die der Entfernung ungenutzten oder redundanten Codes dienen. Auch diese sind zu empfehlen.
Schließlich kann man den Compiler noch dazu bringen, auf die Funktionen moderner Prozessoren zuzugreifen. Neben dem AMD64-Befehlssatz können die Befehlssätze SSE, SSE2, AVX
und AVX2 von MSVC verwendet werden. Standardmäßig ist das Limit auf SSE2 (/arch:SSE2
) gesetzt; Dieser Befehlssatz ist seit fast 15 Jahren auf allen gängigen x86-CPUs
implementiert. Für 64-bit-Programme ist dies ohnehin das Minimum. Wessen Software nur für den Einsatz auf Prozessoren mit AVX oder AVX2 konzipiert ist, der kann hier das Limit
raufsetzen.
Aus Sicherheitsgründen sollte darauf geachtet werden, dass unter „Linker“ → „Erweitert“ die Optionen /NXCOMPAT
(„Datenausführungsverhinderung (DEP)“), /DYNAMICBASE
(„Zufällige Basisadresse“) und /RELEASE
(„Prüfsumme festlegen“)
aktiv sind. Wenn Optimierungsinteressen nicht dagegen sprechen, sollte auch /GS
(s.o., zu finden unter „C/C++“ → „Codegenerierung“)
aktiv sein. Nicht nur aus Sicherheitsgründen, sondern auch zur Verbesserung der Leistung und zur Erhöhung des verwendbaren Speichers von 2 auf 4 GB, sollte bei 32-Bit-Programmen
die Option /LARGEADRESSAWARE
(„Große Adressen aktivieren“ unter „Linker“ → „System“) aktiviert werden. Für 64-bit-Programme
ist die Option allerdings wirkungslos.
Seit Visual C++ 2017 existiert eine Option, um den verwendeten Sprachstandard einzustellen. Dabei wurde auch das Standardverhalten geändert; denn standardmäßig wird nicht dem
aktuellsten implementierten Standard gefolgt. Um das gewohnte Verhalten wiederherzustellen, kann /std:c++latest
genutzt werden.
Für Entwickler von Bibliotheken ist, dass bei statischen Bibliotheken (nicht jedoch bei DLLs) der Linkerschalter /NODEFAULTLIB
(zu finden unter
„Bibliothekar“ → „Allgemein“ unter dem Titel „Alle Standardbibliotheken ignorieren“) aktiviert ist. Andernfalls kommt es zu Konflikten
mit den vom einbindenden Programm genutzen Standardbibliotheken.
Ich rate dazu, die Compileroptionen /std:c++latest /O2 /Zc:inline /Zc:throwingNew
und die Linkeroptionen /NXCOMPAT /DYNAMICASE /OPT:REF /OPT:ICF /RELEASE /LARGEADRESSAWARE
zu setzen. Bei statischen Bibliotheken ist stattdessen die Linkeroption /NODEFAULTLIB
wichtig. Ob man LTCG oder gar PGO aktivieren sollte, hängt stark vom Einzelfall ab.
GCC und Clang bieten eine Unzahl von Optionen, über die das Compilerverhalten im Einzelnen gesteuert werden kann. Einzelheiten können hier
nachgelesen werden. Das grundsätzliche Verhalten wird über die Optimierungsstufen -O0
bis -O3
gesteuert, wobei -O3
zwar am aggressivsten
optimiert, dabei jedoch zuweilen schlechtere Ergebnisse als -O2
erzielt. Daneben existieren noch -Ofast
, was dann nicht standardkonforme Optimierungen
einschließt, -Og
, das nur mit Debugging kompatible Optimierungen auswählt, und -Os
, das die Programmgröße optimiert. Linkzeitoptimierungen können mit
-flto
aktiviert werden.
Sehr interessant sind noch die Möglichkeiten, die Optimierungen an die Zielplatform detailliert anzupassen. Mit -march=
wird das minimal zu unterstützende
Instruktionsset der Ziel-CPU definiert, mit -mtune=
wird festgelegt, für welchen CPU-Typ optmiert wird, ohne dabei die Lauffähigkeit auf anderen CPUs zu
beeinträchtigen, deren Unterstützung mit -march
verlangt wurde. Die möglichen Werte sind hier
aufgelistet. Durch Angabe von native
bei beiden Optionen wird der Wert auf den CPU-Typ des Rechners gesetzt, auf dem der Compiler gerade läuft. Bei
-mtune=
ist außerdem der Wert generic
möglich, der eine nach Auffassung des Compilerherstellers übliche Zielplatform beschreibt. Daneben besteht noch
eine Einstellung über die verwendete Fließkommaeinheit über -mfpmath=
. Mögliche Werte sind 387
(Standard für 32-bit-Kompilate), sse
(Standard für 64-bit-Programme) und both
(von der die Dokumentation jedoch abrät).
Eine eher seltsame Strategie fahren GCC und Clang im Hinblick auf die Unterstützung neuer Sprachstandards. Obwohl die neuen Sprachstandards nahezu vollständig abwärtskompatibel
zu ihren Vorgängern sind, sind sie standardmäßig deaktiviert. Eine Option, stets den aktuellsten vom Compiler unterstützten Sprachstandard zu verwenden, existiert meines Wissens
nach nicht. Weiterhin ändern sich die Bezeichnungen der Schalter, sobald der Standardisierungsprozess abgeschlossen ist (im Fall von C++11 wurde so aus -std=c++0x
-std=c++11
). Damit sind regelmäßige Anpassungen des Buildsystems an die aktuell verwendete Compilerversion unabdingbar und Komplikation bei gleichzeitiger
Unterstützung verschiedener Compilerversionen wohl nicht immer vermeidbar.
GCC hat die unerfreuliche Eigenschaft, dass mehrfach definierte Objekte in verschiedenen Übersetzungseinheiten (ohne Angabe von extern
oder static
)
nicht zu Linkerfehlern führen. Taucht eine solche Definition in Headern auf, die von mehreren Quelldateien eingebunden werden, ist dieses Verhalten vielleicht intuitiv, führt
jedoch bei versehentlicher Vergabe von gleichen Namen für zwei globale Variablen zu äußerst fiesen Fehlern, weil diese beiden Variablen zu einer werden. Deshalb rate ich dringend
dazu, dieses Verhalten durch Angabe von -fno-common
(beim Compiler) bzw. --warn-common
(beim Linker GNU ld) zu ändern.
Ich rate dazu, -O2 -mtune=generic -fno-common
zu verwenden, wenn die erzeugten Dateien auf verschiedenen Systemen laufen sollen, und
-O2 -mtune=native -march=native -fno-common
zu nutzen, wenn das Programm nur auf dem Kompilierungssystem laufen muss (ggf. unter zusätzlicher Angabe von
-mfpmath=sse
, wenn die Zielplatform SSE kann).