Im Folgenden sind unter dem Begriff „Standardoptimierungen“ alle Optimierungsmaßnahmen zusammengefasst, die nur global ein- oder ausgeschaltet, d.h. nicht einzeln beeinflusst werden können.
Demgegenüber gibt es die einzeln beeinflussbaren Optimierungsmaßnahmen, wozu Schleifenexpansion und Inline-Generierung zählen. Diese Optimierungsmaßnahmen können u.a. deshalb einzeln gesteuert werden, weil den durch die Optimierung erzielten Verbesserungen (z.B. schnellere Ablaufzeit) auch u.U. gewisse Nachteile gegenüberstehen, z.B. die Vergrößerung des erzeugten Moduls oder längere Übersetzungszeiten.
Wenn die Optionen LOOP-UNROLLING und INLINING eingeschaltet sind, kann sich die Performance u.U. verschlechtern. Es empfiehlt sich, die für eine Applikation jeweils günstigste Einstellung durch Tests zu ermitteln.
Standardoptimierungen
Der C/C++-Compiler führt folgende Standardoptimierungen durch:
Berechnung konstanter Ausdrücke zur Übersetzungszeit
- Eliminierung unnötiger Zuweisungen
Propagation konstanter Ausdrücke
Eliminierung redundanter Ausdrücke
Optimierung der Indexrechnung in Schleifen
Optimierung von Sprüngen auf unbedingte Sprungbefehle
Ein wichtiger Begriff für die Optimierung ist der Begriff „Basisblock“. Als Basisblock wird eine maximale unverzweigte Befehlsfolge bezeichnet. Eine solche Befehlsfolge hat genau einen Eingangspunkt und einen Ausgangspunkt.
Die Basisblöcke sind die Einheiten, über die die meisten Optimierungsmaßnahmen des C/C++-Compilers realisiert werden.
Berechnung konstanter Ausdrücke zur Übersetzungszeit
Durch die Berechnung von Ausdrücken, deren Operanden während der Übersetzung wertmäßig bekannt sind, wird die Ausführung von Befehlen vom Programmlauf in den Compilerlauf verlagert und damit eine kürzere Programm-Laufzeit erreicht.
Die Berechnungen zur Übersetzungszeit umfassen die Integer-Arithmetik und die Vergleichsoperationen.Beispiel
vor der Optimierung
I = 1 + 2;
I = 2 * 4;
1 <= 5;
nach der Optimierung
I = 3;
I = 8;
<TRUE>
Eliminierung unnötiger Zuweisungen
Wird nach einer Zuweisung an eine Variable v der Wert von v erneut verändert, ohne dass der Wert benutzt wurde, so wird diese Zuweisung eliminiert. Eliminiert werden auch Zuweisungen an Variablen, deren Wert im weiteren Programmablauf nicht mehr verwendet wird.
Beispiel
vor der Optimierung
i = 5;
i = 3;
nach der Optimierung
i = 3;
Propagation konstanter Ausdrücke
Wird in einem Ausdruck eine Variable verwendet, deren Wert zur Übersetzungszeit bereits bekannt ist, so wird diese Variable durch den entsprechenden Wert ersetzt.
Beispiel
vor der Optimierung
a = 3;
i = a;
nach der Optimierung
a = 3;
i = 3;
Diese Optimierung hat auch Auswirkungen auf andere Optimierungstechniken. Nach der erfolgreichen Propagation einer Variablen kann die ursprüngliche Zuweisung eventuell gelöscht werden oder es kann ein neuer konstanter Ausdruck entstehen, der schon zur Übersetzungszeit berechnet wird.
Beispiel
vor der Optimierung
a = 3;
i = a + 4;
a = 5;
nach der Optimierung
i = 7;
a = 5;
Eliminierung redundanter Ausdrücke
Tritt innerhalb eines Basisblockes ein Ausdruck auf, dessen Wert zur Ausführungszeit auf Grund einer früheren Berechnung bereits bekannt ist, so ist dieser Ausdruck redundant. Um die zweite Berechnung zu vermeiden, wird der Ausdruck einer neu eingeführten Variablen zugewiesen und an allen Stellen durch diese neue Variable ersetzt.
Beispiel
vor der Optimierung
a = b * c + 20;
e = b * c - 10;
nach der Optimierung
h = b * c;
a = h + 20;
e = h - 10;
Optimierung der Indexrechnung in Schleifen
Wird ein Arrayelement in einer Schleife über eine Iterationsvariable indiziert, so wird die zur Berechnung der Adresse des Arrayelements notwendige Multiplikation auf Additionen zurückgeführt. Normalerweise errechnet sich die Adresse des Arrayelements aus
Basisadresse + Index * Länge eines Arrayelements
Der Optimierer versorgt vor dem Schleifeneingang eine Adressvariable mit der Adresse des beim ersten Schleifendurchlauf angesprochenen Arrayelements. Bei jedem Schleifendurchlauf wird auf diese Adressvariable eine feste Schrittweite in der Länge eines Array-Elements addiert.
Besonders lohnend ist diese Optimierung bei mehrdimensionalen Arrays, da in günstigen Fällen pro Dimension eine Multiplikation eingespart wird.
Optimierung von Sprüngen auf unbedingte Sprungbefehle
In Sprungbefehlen, die als Sprungziel einen unbedingten Sprung haben, wird das Sprungziel durch das des unbedingten Sprunges ersetzt. In diesem Zusammenhang wird auch überflüssiger, weil unerreichbarer Code eliminiert.
Beispiel
vor der Optimierung
goto lab1; ... lab1: goto lab2; ... lab2:
nach der Optimierung
goto lab2; ... lab1: goto lab2; ... lab2:
Inline-Generierung von benutzereigenen Funktionen
Die Inline-Generierung einer Funktion macht den Aufruf einer Funktion zum Ablaufzeitpunkt überflüssig, da der Funktionscode an den Aufrufstellen in den Code der rufenden Funktion integriert wird. Damit können zeitaufwendige Codefolgen für Funktions-Aufrufe und -Rücksprünge eingespart werden (z.B. Register retten und restaurieren, Stack allokieren, Parameter in den Übergabebereich schreiben). Dadurch ergeben sich z.T. beträchtliche Laufzeiteinsparungen. Außerdem erhöht die Inline-Generierung von Funktionen die Wirkung der Standardoptimierungen durch den vergrößerten Kontext. Inline-generierte static
-Funktionen werden gelöscht.
Durch die Inline-Generierung nimmt jedoch auch die Größe der erzeugten Module zu. Durch Inline-Generierung können so große Funktionen entstehen, dass immer mehr Variablen als potenzielle Registerkandidaten möglich werden, die jedoch wegen der in beschränkter Zahl zur Verfügung stehenden Register nicht alle bedient werden können. Dieser Sachverhalt ist gegenüber den Vorteilen der Optimierungsmaßnahme abzuwägen.
Für die Inline-Generierung eignen sich vor allem kleine Funktionen, da dort der Overhead für die Funktionsaufrufe und -rücksprünge gegenüber dem eigentlichen Funktionscode einen großen Anteil einnimmt.
Funktionen mit folgenden Eigenschaften werden in keinem Fall inline generiert:
Funktionen mit einer variablen Anzahl von Parametern (vgl. va_...-Makros in <stdarg.h>)
Funktionen, die setjmp-Aufrufe enthalten
rekursive Funktionen
Expansion von Schleifen
Durch Schleifenexpansion wird die Zahl der Schleifendurchläufe verringert, indem der Schleifenrumpf (Anweisungsblock) ein oder mehrere Male wiederholt, „expandiert“ wird. Da bei jedem Schleifendurchlauf Schleifensteueranweisungen ausgeführt werden müssen, die den Wert des aktuellen Schleifendurchlaufs prüfen und entsprechend verzweigen, verbessert eine Verringerung der Schleifendurchläufe die Ausführungszeit.
Wird beispielsweise der Schleifenrumpf verdoppelt (Expansionsfaktor 2), so halbiert sich der Aufwand für Schleifensteueranweisungen. Allgemein gilt: Bei einem Expansionsfaktor n verringert sich der Aufwand für Schleifensteueranweisungen auf 1/n.
Das erzeugte Modul wird jedoch durch die Code-Wiederholungen vergrößert.
Der Optimierer verwendet standardmäßig den Expansionsfaktor 4.
Mit dem expandierten Schleifenrumpf entsteht außerdem neues Optimierungspotential. In den verlängerten Basisblöcken sind Verbesserungen z.B. durch die Propagation konstanter Ausdrücke oder durch die Eliminierung redundanter Ausdrücke möglich.
Beispiel für eine Schleifenexpansion mit Expansionsfaktor 4
Vor der Expansion:
i = 0; while(i < 80) { a[i] = b[i+1]; i++; }
Nach der Expansion:
i = 0; while(i < 80) { a[i] = b[i+1]; i++; if(!(i < 80)) goto ende; a[i] = b[i+1]; i++; if(!(i < 80)) goto ende; a[i] = b[i+1]; i++; if(!(i < 80)) goto ende; a[i] = b[i+1]; i++; } ende:
Hier sind Optimierungen im Schleifenrumpf möglich.