Codeoptimierung mit Visual C++

Veröffentlicht: 20. Jun 2003 | Aktualisiert: 24. Jun 2004

Von Mark Lacey

Auf dieser Seite

Einführung Einführung
Visual C++ .NET 2003 Visual C++ .NET 2003
Visual C++ .NET 2002 Visual C++ .NET 2002
Optimierungsmethoden Optimierungsmethoden
Verbesserte Compileroptionen in Visual C++ .NET Verbesserte Compileroptionen in Visual C++ .NET
Schlussfolgerung Schlussfolgerung

Einführung

Es ist immer wieder frustrierend, mit einem neuen Tool zu arbeiten und sich zu fragen, ob man es auch wirklich optimal nutzt. In diesem Whitepaper wird versucht, Unsicherheiten hinsichtlich der Arbeit mit dem Visual C++-Optimierer zu zerstreuen, so dass Sie diesen bestmöglich einsetzen können.

 

Visual C++ .NET 2003

In der Visual C++ .NET 2003-Version gibt es zwei neue leistungsbezogene Compileroptionen und zusätzlich Verbesserungen einiger Optimierungsfunktionen, die bereits in Visual C++ .NET 2002 zur Verfügung standen.

Bei der ersten der neuen leistungsbezogenen Optionen handelt es sich um /G7. Dadurch wird der Compiler veranlasst, den Code für den Intel Pentium 4- und AMD Athlon-Prozessor zu optimieren.

Bei der Kompilierung einer Anwendung mit /G7 kann die Leistungssteigerung zwar unterschiedlich ausfallen, doch verglichen mit dem von Visual C++ .NET 2002 generierten Code ist eine Verkürzung der Ausführungszeit um 5 bis 10 Prozent bei typischen Programmen nicht selten. Bei Programmen mit viel Gleitkommacode ist sogar eine Verkürzung zwischen 10 und 15 Prozent möglich. Die Leistungssteigerung kann stark variieren, wobei in einigen Fällen durchaus eine Steigerung um 20 Prozent möglich ist, wenn die Kompilierung mit /G7 erfolgt und die neueste Prozessorgeneration verwendet wird.

Die Verwendung von /G7 impliziert nicht automatisch, dass der vom Compiler erzeugte Code nur auf einem Intel Pentium 4- oder AMD Athlon-Prozessor ausgeführt werden kann. Der mit /G7 kompilierte Code kann auch weiterhin auf älteren Prozessoren ausgeführt werden, wobei jedoch mit gewissen kleinen Leistungseinschränkungen zu rechnen ist. Außerdem wird der mit /G7 kompilierte Code in einigen Fällen auf dem AMD Athlon-Prozessor langsamer ausgeführt.

Wenn keine Option /Gx angegeben wird, verwendet der Compiler standardmäßig den "gemischten" Optimierungsmodus /GB. In der 2002- und 2003-Version von Visual C++ .NET entspricht /GB dem Modus /G6, der den Code für Intel Pentium Pro-, Pentium II- und Pentium III-Prozessoren optimiert.

Ein Beispiel für die Verbesserungen bei Verwendung von /G7 ist die größere Auswahl an Anweisungen für den Intel Pentium 4-Prozessor bei der Ganzzahlmultiplikation mit einer Konstanten. Sehen Sie sich z.B. folgenden Code an:

int i; 
... 
// Do something that assigns a value to i.  
... 
return i*15;

Bei Kompilierung mit der Standardoption /G6 sieht das Ergebnis wie folgt aus:

mov   eax, DWORD PTR _i$[esp-4] 
imul   eax, 15

Bei der Kompilierung mit /G7 wird eine schnellere (jedoch längere) Sequenz erzeugt, mit der die Verwendung der imul-Anweisung verhindert wird, die auf dem Intel Pentium 4 eine Latenz von 14 Zyklen aufweist:

mov   ecx, DWORD PTR _i$[esp-4] 
mov   eax, ecx 
shl   eax, 4 
sub   eax, ecx

Bei der zweiten leistungsbezogenen Option handelt es sich um /arch:[argument]. Hier wird das Argument SSE oder SSE2 verwendet. Mit dieser Option stehen dem Compiler SSE-Anweisungen (Streaming SIMD Extensions) und SSE2-Anweisungen (Streaming SIMD Extensions 2) sowie weitere neue Anweisungen zur Verfügung, die auf Prozessoren verfügbar sind, die SSE und/oder SSE2 unterstützen. Bei der Kompilierung mit /arch:SSE wird der Ergebniscode nur auf Prozessoren ausgeführt, die die SSE-Anweisungen sowie CMOV, FCOMI, FCOMIP, FUCOMI und FUCOMIP unterstützen. Entsprechend wird bei einer Kompilierung mit /arch:SSE2 der Ergebniscode nur auf Prozessoren ausgeführt, die die SSE2-Anweisungen unterstützen.

Wie bei /G7 schwankt die Leistungssteigerung bei Kompilierung einer Anwendung mit /arch:SSE oder /arch:SSE2. Normalerweise verkürzt sich die Ausführungszeit um 2 bis 3 Prozent, in einigen Extremfällen wurde sogar eine um 5 Prozent kürzere Ausführungszeit gemessen.

Die Option /arch:SSE hat die folgenden spezifischen Merkmale:

  • Verwendung der SSE-Anweisungen für Gleitkommavariablen einfacher Genauigkeit (float), wenn dadurch eine Leistungssteigerung erfolgt.

  • Verwendung der CMOV-Anweisung, die erstmals im Intel Pentium Pro-Prozessor eingeführt wurde.

  • Verwendung der FCOMI-, FCOMIP-, FUCOMI- und FUCOMIP-Anweisungen, die ebenfalls erstmals im Pentium Pro-Prozessor eingeführt wurden.

Die Option /arch:SSE2 umfasst neben allen Merkmalen der Option /arch:SSE zusätzlich folgende Merkmale:

  • Verwendung der SSE2-Anweisungen für Gleitkommavariablen doppelter Genauigkeit (double), wenn dadurch eine Leistungssteigerung erfolgt.

  • Verwendung von SSE2-Anweisungen für 64-Bit-Verschiebungen.

Zusätzlich zu diesen Vorteilen verwendet der Compiler beim Erstellen von Builds mit Hilfe der Option /GL (Optimierung des ganzen Programms) und /arch:SSE oder /arch:SSE2 benutzerdefinierte Aufrufkonventionen für Funktionen mit Gleitkommaargumenten und Rückgabewerten.

Schließlich wurden einige der in Vorgängerversionen des Produktes eingeführten Optimierungen in Visual C++ .NET 2003 verbessert. Eine dieser Verbesserungen betrifft die Fähigkeit, die Übergabe von "toten" Parametern zu unterbinden, d.h. von Parametern, auf die in der aufgerufenen Funktion nicht verwiesen wird. Beispiel:

int 
f1(int i, int j, int k) 
{ 
   return i+k; 
} 
int 
main() 
{ 
   int n = a+b+c+d; 
   m = f1(3,n,4); 
   return 0; 
}

In der Funktion f1() wird der zweite Parameter nicht verwendet. Beim Kompilieren mit der Option /GL (Optimierung des ganzen Programms) erzeugt der Compiler beim Aufruf an f1() in main() in etwa folgenden Code:

mov   eax, 4 
mov   ecx, 3 
call   <A href="mailto:?f1@@YAHHHH@Z">?f1@@YAHHHH@Z</A> 
mov   DWORD PTR <A href="mailto:?m@@3HA">?m@@3HA</A>, eax

In diesem Beispiel wird die Berechnung des Wertes 'n' nie durchgeführt, und nur die beiden Parameter, auf die in f1() verwiesen wird, werden an f1() übergeben. (Dabei werden sie in Registern und nicht im Stapel übergeben.) Außerdem wurde bei diesem Beispiel mit deaktiviertem Inlining kompiliert, da bei aktiviertem Inlining der Aufruf komplett "wegoptimiert" wird und der übrige Code den Wert 'm' auf 7 setzt.

 

Visual C++ .NET 2002

In der 2002-Version von Visual C++ .NET wurde die Möglichkeit zur Optimierung des ganzen Programms (Whole Program Optimization, WPO) mit Hilfe der Compileroption /GL eingeführt. Dabei wird eine Zwischendarstellung des Programms (und nicht der Objektcode) in den Objektdateien gespeichert, die vom Compiler erstellt werden. Anschließend ruft der Linker den Optimierer und Codegenerator während der Link-Time auf, um während der Optimierung die Informationen des Gesamtprogramms zu nutzen. Dies alles läuft völlig transparent ab und bringt nur minimale Änderungen des Buildprozesses mit sich.

Einer der Hauptvorteile von WPO liegt in der Möglichkeit, Inline-Funktionen über Quellmodule hinweg auszuführen. Damit gelingt es dem Inliner, die Leistung zu verbessern, indem kleine Funktionen komplett über eine ausführbare Datei hinweg eingefügt werden. Weitere Vorteile von WPO liegen in der genaueren Verfolgung von Speicheraliasing und Registerverwendung, um ein Speichern und erneutes Laden bei Funktionsaufrufen zu vermeiden.

Das folgende Beispiel zeigt einige der Verbesserungen im generierten Code, die sich aus dem Einsatz von WPO ergeben.

// File 1 
extern void func (int *, int *); 
int g, h; 
int 
main() 
{ 
   int i = 0; 
   int j = 1; 
   g = 5; 
   h = 6; 
   func(&i, &j); 
   g = g + i; 
   h = h + i; 
   return 0; 
} 
// File 2 
extern int g; 
extern int h; 
void 
func(int *pi, int *pj) 
{ 
   *pj = g; 
   h = *pi; 
}

Beim Kompilieren dieses Beispiels ohne die Option /GL wird Code generiert, der dem folgenden, für Datei 1 generierten Code ähnelt:

sub   esp, 8 
lea   eax, DWORD PTR _j$[esp+8] 
push   eax 
lea   ecx, DWORD PTR _i$[esp+12] 
push   ecx 
mov   DWORD PTR _i$[esp+16], 0 
mov   DWORD PTR _j$[esp+16], 1 
mov   DWORD PTR <A href="mailto:?g@@3HA">?g@@3HA</A>, 5 
mov   DWORD PTR <A href="mailto:?h@@3HA">?h@@3HA</A>, 6 
call   <A href="mailto:?func@@YAXPAH0@Z">?func@@YAXPAH0@Z</A> 
mov   eax, DWORD PTR _i$[esp+16] 
mov   edx, DWORD PTR <A href="mailto:?g@@3HA">?g@@3HA</A> 
mov   ecx, DWORD PTR <A href="mailto:?h@@3HA">?h@@3HA</A> 
add   edx, eax 
add   ecx, eax 
mov   DWORD PTR <A href="mailto:?g@@3HA">?g@@3HA</A>, edx 
mov   DWORD PTR <A href="mailto:?h@@3HA">?h@@3HA</A>, ecx 
xor   eax, eax 
add   esp, 16 
ret   0

Beim Kompilieren mit /GL wird Code generiert, der dem im Folgenden dargestellten ähnelt. Beachten Sie, dass zahlreiche Anweisungen gelöscht wurden. Außerdem ist zu beachten, dass dieses konstruierte Beispiel mit /Ob0 (kein Inlining) kompiliert wurde, da bei aktiviertem Inlining das gesamte Beispiel "wegoptimiert" wird.

sub   esp, 8 
lea   ecx, DWORD PTR _j$[esp+8] 
lea   edx, DWORD PTR _i$[esp+8] 
mov   DWORD PTR _i$[esp+8], 0 
mov   DWORD PTR <A href="mailto:?g@@3HA">?g@@3HA</A>, 5 
mov   DWORD PTR <A href="mailto:?h@@3HA">?h@@3HA</A>, 6 
call   <A href="mailto:?func@@YAXPAH0@Z">?func@@YAXPAH0@Z</A> 
mov   DWORD PTR <A href="mailto:?g@@3HA">?g@@3HA</A>, 5 
xor   eax, eax 
add   esp, 8 
ret   0

Abgesehen von der neuen Option /GL wurde der Inliner im Optimierer so verbessert, dass der Compiler beim Kompilieren mit /Ox, /O1 oder /O2 standardmäßig wählen kann, bei welchen Funktionen das Inlining angewendet werden soll. Dies war bei früheren Versionen von Visual C++ nicht der Fall, da nur solche Funktionen standardmäßig berücksichtigt wurden, die als "inline" oder "__inline" gekennzeichnet waren. Die alte Verhaltensweise kann erreicht werden, wenn die Option /Ob1 nach Wahl der primären Optimierung (/Ox, /O1 oder /O2) hinzugefügt wird.

 

Optimierungsmethoden

Der Visual C++-Compiler beinhaltet die zwei primären Optimierungsoptionen /O1 und /O2. Die Bedeutung von /O1 ist "Größe minimieren". Dies schlägt sich in der individuellen Aktivierung der folgenden Optionen nieder:

  • /Og - Globale Optimierung aktivieren

  • /Os - Kompakten Code bevorzugen

  • /Oy - Rahmenzeigerauslassungen aktivieren

  • /Ob2 - Compiler wählt mögliche Inlinefunktionen

  • /GF - Schreibgeschützten Zeichenfolgenpool aktivieren

  • /Gy - Funktionslevel-Linking aktivieren

Die Option /O2 bedeutet "Geschwindigkeit maximieren" und ähnelt der Option /O1. Die einzige Ausnahme besteht darin, dass statt /Os die Option /Ot ("Schnellen Code bevorzugen") angegeben wird und /Oi ("Systeminterne Funktionen aktivieren") ebenfalls aktiviert ist.

Im Allgemeinen sollten kleine Anwendungen mit /O2 und große Anwendungen mit /O1 kompiliert werden, da sehr große Anwendungen den Anweisungscache des Prozessors stark belasten können und somit die Leistung beeinträchtigen. Um die Beinträchtigungen so gering wie möglich zu halten, verwenden Sie /O1 zur Reduzierung des Codeumfangs, der vom Optimierer bei einigen Transformationen, wie Entrollen der Schleife oder Auswahl von größeren, schnelleren Codesequenzen, verwendet wird.

In der Regel bieten sich die durch /O1 deaktivierten Optimierungen bei kleinen Codesequenzen an. Wenn sie jedoch uneingeschränkt bei großen Anwendungen verwendet werden, können sie die Codegröße dermaßen aufblähen, dass es aufgrund der Problematik mit dem Anweisungscache zum vollständigen Datenverlust kommen kann.

Nach Auswahl der primären Optimierungsoption sollte der Code profiliert werden, um nach Hotspots zu suchen. Anschließend können verschiedene Optimierungsoptionen auf diesen Code angewendet werden. Vor allem, wenn Sie /O1 als primäre Optimierungsoption für alle Dateien verwenden möchten, empfiehlt es sich, kritische Funktionen so anzupassen, dass sie in "Geschwindigkeit maximieren" kompiliert werden.

Der Visual C++-Compiler unterstützt ein Optimierungspragma zur Anpassung der Optimierungssteuerung bei spezifischen Funktionen.

Nehmen wir z.B. einmal an, Sie haben Ihre Anwendung mit /O1 kompiliert, und beim Profilieren der Anwendung zeigt sich, dass die Funktion fiddle () eine der kritischen Funktionen des Programms ist. Sie können folgendermaßen vorgehen:

#pragma optimize("t", on) 
int fiddle(S *p) 
{ 
   ...; 
} 
#pragma optimize("", on)

Dadurch wird die Funktion mit zwei Optimierungspragmen gewrappt. Das erste Pragma ändert den Optimierungsmodus in "Geschwindigkeit maximieren", und das zweite Pragma stellt den in der Befehlszeile angegebenen Optimierungsmodus wieder her.

Der Lieferumfang von Visual C++ umfasst zwar keinen Profiler, doch können Sie die DevPartner Profiler Community Edition der Compuware Corporation unter folgendem URL finden:
http://www.compuware.com/products/devpartner/profiler (in Englisch)

Zusätzlich zu den Optionen /O1 und /O2 gibt es die Option /Ox, die ähnlich wie /O2 funktioniert und mit /Os kombiniert werden kann, um ein ähnliches Ergebnis wie /O1 zu erzielen. Es empfiehlt sich, statt /Ox die Optionen /O1 und /O2 zu verwenden, da /O1 und /O2 mehr Vorteile bieten.

Weiter vorne in diesem Artikel wurden die Compileroptionen /G7, /arch und /GL erläutert.

Zusätzlich zu diesen Optimierungsoptionen stellt Visual C++ folgende Optionen zur Verfügung:

  • /GA zur Optimierung statischer lokaler Speicherzugriffe von Threads (Thread Local Storage, TLS) auf ausführbare Dateien.
    Anmerkung Verwenden Sie diese Option nicht, wenn Sie Code kompilieren, der in einer DLL resultiert.

  • /Gr aktiviert standardmäßig die __fastcall-Aufrufkonvention, wodurch die beiden ersten Parameter gegebenenfalls in Register übergeben werden.

Diese beiden Optionen bringen zusätzliche Vorteile für optimale Codebuilds mit sich. Probieren Sie sie einfach mal aus, und messen Sie die erzielten Ergebnisse.

Eine weitere nützliche Option ist /opt:ref, die an den Linker übergeben werden kann. Mit dieser Option werden nicht verwiesene Funktionen und Daten eliminiert sowie die Option /opt:icf aktiviert, mit der versucht wird, identische Funktionen (z.B. jene, die aus der Erweiterung von Vorlagen entstehen) zu kombinieren, um die Codegröße noch weiter zu verringern.

 

Verbesserte Compileroptionen in Visual C++ .NET

Beim Erstellen vom Builds mit Visual C++ .NET 2003 sollte bei den meisten Projekten mit drei wichtigen Compileroptionen gearbeitet werden. Jede dieser Optionen gab es bereits in Visual C++ .NET 2002, doch wurden sie in der 2003-Produktversion weiter verbessert.

In der folgenden Tabelle wird die Einsatzmöglichkeit dieser Compileroptionen kurz beschrieben. In der Visual C++-Dokumentation finden Sie weitere Einzelheiten.

Compiler Verwendung /RTC1 Verwendung in (nicht optimierten) Debugbuilds. Fügt Laufzeitprüfungen ein, um Codierungsfehler aufzufinden (wie Verwendung von nicht initialisiertem Speicher oder Mischen von __stdcall- und __cdecl-Aufrufkonventionen). /GS Fügt Prüfungen ein, um Pufferüberläufe zu erkennen, die die Rückgabeadresse der aktuellen ausgeführten Funktion überschreiben. Versucht, "Überfälle" bösartigen Codes auf die Rückgabeadresse zu verhindern.Anmerkung Dies ist weder eine Patentlösung noch ein Ersatz für eine gute Codierung und sorgfältiges Testen. Sie sollten stets Ihren Code überprüfen, um sicherzustellen, dass es nicht zu Pufferüberläufen kommt. /Wp64 Erkennt im Code vorhandene 64-Bit-Portabilitätsprobleme. Mit dieser Compileroption wird der spätere Wechsel zu einem 64-Bit-System vereinfacht.

 

Schlussfolgerung

Visual C++ .NET 2003 beinhaltet zwei neue Optimierungsoptionen zur Leistungssteigerung sowie Optimierungen der im Lieferumfang von Visual C++ .NET 2002 enthaltenen Funktionen. Durch Kenntnis dieser Funktionen sowie der drei Compileroptionen, die möglichst bei jedem von Visual C++ kompilierten Projekt angewendet werden sollten, werden Ihre Codebuilds optimiert.