Letzte relevante Änderung: 08.07.2022

Compilerschalter

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.

Inhaltsverzeichnis:

Microsoft Visual C++ (MSVC)

MSVC

Optimierungen

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.

Sonstige Einstellungen

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.

Kurzfassung

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.

GNU Compiler Collection (GCC), Clang

GCC, Clang
Da die Kommandozeilenschnittstellen und Verhalten von GCC und Clang kompatibel bzw. ähnlich sind, werden beide in einem Text behandelt.

Optimierungen

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).

Sonstige Einstellungen

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.

Kurzfassung

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).