Tipps zur Leistungsoptimierung in C++

In diesem Tutorial möchte ich ein paar Tipps geben, wie man den Code beschleunigen kann.
Bevor man anfängt irgendwas zu optimieren, sollte man ein paar Regeln beachten.

Generell kann man sagen, dass man nicht optimieren sollte, wenn das Spiel schon schnell genug läuft. Was bringt es schon, wenn man das Spiel von 70 auf 80 fps beschleunigt wird?! Der Spieler merkt davon nichts. Man sollte die Zeit lieber in was anderes investieren z.B. um mehr Features einzubauen oder Bugs zu entfernen.

Zudem sollte man nicht immer drauf los optimieren, sondern zuerst den Flaschenhals des Programms herausfinden und dann diesen optimieren. Ansonsten kann es sein, dass man mehrere Stunden oder gar Tage am Optimieren einer Funktion verbringt, die vielleicht langsam ist, aber nur sehr selten aufgerufen wird und somit keine Auswirkung auf die Gesamtgeschwindigkeit des Programms hat. In meisten Fällen sind es nur wenige Funktionen, an denen man feilen muss, um das Programm deutlich zu beschleunigen. Der Flaschenhals lässt sich leicht mit einem Profiler herausfinden. Dazu kann ich einen kostenlosen Profiler CodeAnalyst von AMD empfehlen.

Die hier im Folgenden aufgeführte Optimierungen sollten nicht als ein Muss, sondern als ein Tipp betrachtet werden. Einige der Optimierungen sind mit mehr Schreibaufwand verbunden oder erschweren das Lesen des Quelltextes. Die anderen wiederum bringen nicht nur Leistungszuwachs, sondern auch mehr Klarheit und Ordnung in den Code beziehungsweise sehen einfach besser aus und zeigen mehr Professionalität.

Wenn Sie einen Tipp kennen, der Leistung bring, dann schreiben Sie mir einfach.

Die Reihenfolge der Tipps hat keinen tieferen Sinn (Tipp 1 ist nicht wichtiger als Tipp 2).

1. Division vermeiden

Eine mathematische Division wird von der CPU bis zu 39-mal langsamer ausgeführt als eine Multiplikation.

Langsam:

 a = a/2.0f; 

Schnell:

 a = a * 0.5f; 

Hinweis: Eine Division zweier Integers ist noch schneller als eine Multiplikation mit einer Gleitkommazahl, weil die FPU nicht beansprucht wird.

2. Werte im Voraus berechnen

Wenn man weiß, dass ein berechneter Wert ein paar Zeilen später wieder gebraucht wird, dann sollte man ihn besser in eine Variable ablegen und wieder verwenden, anstatt diesen neu zu berechnen.

Langsam:

x = 24*n*n;
y = n*n / z;

Schnell:

float quadrat = n*n;
x = 24 * quadrat;
y = quadrat / z;

3. Daten niemals als Kopie, sondern als Referenz oder Zeiger an Funktionen übergeben

Dadurch erspart man sich die langsamen Kopiervorgänge.

Langsam:

void function(float p1, XY p2);

Schnell:

void function(const float &p1, const XY &p2);

4. 32-Bit Variablen verwenden

Aktuelle CPUs haben eine 32-Bit Architektur und arbeiten mit 32-Datenpaketen schneller als mit 8- oder 16-Bit. 64-Bit Prozessoren arbeiten mit 64-Bit Datenpaketen schneller. Man sollte diesen Vorteil ausnutzen.

Langsam:

short auto_speed;

Schnell:

int auto_speed;

5. Multiplikationen und Divisionen durch Bit-Shifts ersetzt

Bei Integern kann man die Multiplikation und Division durch Bit-Shift Operator ersetzen. Das geht aber nur, wenn der Teiler oder Multiplikator der Zweierpotenz entspricht. Das ist oft der Fall, wenn man mit Texturengrößen(64, 128, …) arbeitet. Dabei gilt:

[math]
y\ll x \widehat{=} y*2^{x} \\
y\gg x \widehat{=} y/2^{x}
[/math]

Langsam:

// Multiplikation
int m = x * 64;

// Division
int d = x / 1024;

Schnell:

// Multiplikation (2^6 = 64)
int m = x << 6;

// Division (2^10 = 1024)
int d = x >> 10;

6. Standardimplementierung der trigonometrischen Funktionen vermeiden

Die Standardimplementierungen von cos, sin und tan sind sehr langsam, weil sie eine hohe Genauigkeit besitzen. Meistens reicht jedoch eine Genauigkeit auf zwei Nachkommastellen bereits aus.
Durch Approximation bekommt man ziemlich gute Näherungen. Schauen Sie sich dazu mein Tutorial: Schnelle trigonometrische Funktionen an.

7. Das const-Schlüsselwort verwenden

Man sollte das Schlüsselwort const immer verwenden. Das bringt nicht nur Geschwindigkeitsvorteile, sondern ist auch ein wichtiges Element eines guten Programmierstils. Es gibt drei Situationen wo const verwenden werden kann.

a) const für die Konstantendeklaration

Langsam & unsicher:

int konstante = 640;

Schnell & sicher:

 const int konstante = 640;

b) const für die Paramerübergabe (siehe Tipp 3)

c) const für den Rückgabedatentypen

8. Casts vermeiden

Die Konvertierung von einem Datentypen in einen anderen beansprucht viel Leistung. Vor allem Konvertierung von float zu int ist Zeitintensiv. Am besten man verwendet Integer mit Integern und Float mit Floats.

9. Köpfchen einschalten

Mache mathematische Ausdrücke können ein wenig vereinfacht werden. Der Code wird nicht nur schneller, es lässt sich auch noch besser lesen.

Langsam:

int e = a*b + a*c;

Schnell:

int e = a*(b+c);

10. Inline-Schlüsselwort verwenden

Bei kleinen Funktionen(Methoden), die oft aufgerufen werden, sollte man inline-Schlüsselwort verwenden.

11. Strukturen optimal schreiben

Langsam:

scruct langsam
{
    bool a;     // 1 Byte
    long b;     // 4 Byte
    bool c;     // 1 Byte
};
// sizeof( langsam ) = 12 Byte

Warum 12 Byte? Weil der Compiler jede bool-Variable auf 4 Byte erweitert. Stehen mehrere bools nacheinander, wird nur am Ende der Struktur was dazu gerechnet, damit man auf 4 Byte kommt.

Schnell:

scruct langsam
{
    bool a;
    bool c;
    long b;
};
// sizeof( schnell ) = 8 Byte

Das ganze funktioniert auch bei Klassenattributen.

Das war’s zunächst mal. Mit der Zeit werden weitere Tipps folgen.

Mfg Maxim.

5 Gedanken zu „Tipps zur Leistungsoptimierung in C++“

  1. Sehr interessant und hilfreich, danke, eine Fortsetzung würde mich freuen ;)

    P.S.: Du musst mehr Werbung für diesen fantastischen Blog machen!

  2. Eine Frage hätte ich nun noch. Und zwar wird in 3. behauptet, dass float& schneller sei als float. Ist dies wirklich der Fall? Denn sowohl floats als auch Referenzen sind doch beide 4 Byte groß, oder irre ich mich?
    Ist das anlegen der Kopie, das was es langsamer macht?

    Und was genau bewirkt das const performancetechnisch, ich dachte, das würde nur nach außen hin „sagen“, dass die übergeben Referenzen innerhalb der Funktion nicht verändert werden?

  3. Das mit float ist nur ein Syntaxbeispiel, damit man eher mit dem XY klar kommt, dass daneben steht, da wie du es richtig bemerkt hast, dass die Referenz genauso groß ist wie der der Wert selbst. Bei Integern würde es keinen Unterschied machen, aber bei floats bin ich mir jetzt ganz sicher ob es da nicht doch einen Mehraufwand gibt, da bei den Fließkommazahlen noch die FPU beansprucht wird. Man müsste sich den AMS-Code anschauen. Ich denke bei nativen Datentypen kann man ruhig eine Kopie verwenden, bei Strukturen und Klassen auf jeden Fall eine Referenz.

    Das const ist nicht nur für den Programmierer, sondern auch für den Compiler da. Wenn man weiß, dass der Wert nicht verändert wird, können eher Optimierungen durchgeführt werden (z.B. Werte im Cache abspeichern). Es ist also eine Sache der Compileroptimierung. Auf jeden Fall gehört es zu einem guten Stil const zu verwenden, auch im Bezug auf Methoden (wenn mehrere Threads auf gemeinsame Daten zugreifen, dann ist es noch wichtiger).

    Trotz aller Optimierungstipps der Welt:
    1) Zuerst das Programm funktionsfähig machen, dann optimieren.
    2) Immer (!) zuerst einen Profiler benutzen um den Flaschenhals zu finden.

Schreibe einen Kommentar