titelk.jpg (13599 Byte)

Tutorial 1: Programmoptimierungen

Willkommen zum ersten rand()-Tutorial von Kongo. Das erste Tutorial ist der Optimierung von Programmen gewidmet, um noch schnelleren Code zu schreiben. Bei irgendwelchen Fragen, Beschwerden, Wünschen (z.B. fürs nächste Tutorial) schreib an kongo@gmx.at. Wenn du für diese Kapitel noch Informationen hast und du denkst dir: "Das muss auch noch rein!", dann schreib mir. Ich werde dieses Tutorial dann nachbessern. Feedback ist Willkommen.

Jedes Spiel, das man programmiert, soll noch schneller und besser werden. Jedoch kann der Code den man selbst schreibt, schnell die Performance drücken. Wie kann man noch schnelleren Code schreiben? Dies sind nur Aufzählungen von Programmierkniffen, also probier einfach herum.

Mathematische Tricks

Misc

Zeiger-Dereferenzierung

Code wie dieser:

for (int i = 0; i < Num; i++) {
    Render_Context->Back->Surf->bit[i] = Val;
}

kann durch folgenden ersetzt werden:

unsigned char *Surf_Bits = Render_Context->Back->Surf->Bit[0];
for (int i = 0; i < Num; i++) {
    Surf_Bits[i] = Val;
}

Dadurch ersparst du dir die ganzen Zeiger Dereferenzierungen, die ja eigentlich unnötig sind.

Schnellere Klassen

Unser Beispiel für dieses Unterkapitel ist eine Klasse für Vektoren. Wir haben ein paar Operatoren überladen, einen Konstruktor, Kopierkonstruktor und einen Destruktor geschrieben.

class Vector3
{
   float     m_x;
   float     m_y;
   float     m_z;
   Vector3();
   Vector3(Vector3& vec);
   ~Vector3();
   Vector3 operator+(Vector3 vec);
   Vector3 operator*(Vector3 vec);
};

So.. das erste was wir hier überlegen ist, brauchen wir den Destruktor? Der Compiler kann einen mindestens gleich schnellen Leer-Destruktor erstellen. Wir haben nichts, dass einen Destruktor benötigt. Des weiteren können wir versuchen die Operatorfunktionen schneller zu machen. Hier ein Beispiel für eine nicht sehr gut durchdachte Funktion des Operator +.

Vector3 operator+(Vector3 vec)
{
    Vector3 retVec;
    retVec.m_x = m_x + vec.m_x;
    retVec.m_y = m_y + vec.m_y;
    retVec.m_z = m_z + vec.m_z;
    return retVec;
}

Hier gibt es sehr viel zu optimieren. Fangen wir einfach ganz oben an. Der Parameter wird per Wert übergeben. Daraus folgt, dass Speicher allokiert wird und der Kopierkonstruktor aufgerufen wird. Besser wäre es, eine Referenz auf den Vektor zu machen. Des weiteren haben wir eine Variable bereitgestellt, die die neuen Werte nimmt und dann wieder zurückgibt. Daraus folgt, dass wir erstens wieder einen Aufruf eines Konstruktors haben, obwohl wir die Werte ja sowieso ändern und zweitens wird am Schluß der Kopierkonstruktor aufgerufen, da retVec ja eine lokale Variable ist.

Unsere optimierte Funktion sieht wie folgt aus:

Vector3 operator+( const Vector3 &vec) const
{
    return Vector3(m_x + vec.m_x, m_y + vec.m_y, m_z + vec.m_z);
}

Zusätzlich haben wir einen neuen Konstruktor geschrieben, der die drei Werte annimmt. Wir ersparen uns in der optimierten Funktion drei Kopierkonstruktoraufrufe. Das ist zwar nicht viel Ersparnis im Großen, jedoch für eine so kleine Funktion kann dies eine große Zeitersparnis bedeuten.

SIMD

SIMD (oder auch Intel's Streaming SIMD Extensions) bedeutet Single Instruction Multiple Data. Also eine Instruktion, mehrere Daten. SIMD ist eine Erweiterung der Pentium-Prozessoren, durch die wir noch schnelleren Code schreiben können.

Mit den SIMD Extensions können wir zB. 4 Multiplikationen oder 4 Additionen auf einmal berechnen. Ein SIMD Register kann 4 floating-point Variablen beinhalten. Also können wir in ein Register einen ganzen Vektor laden, oder in vier Register eine ganze 4x4-Matrix.

Für eine Matrix*Matrix Funktion benötigen wir standardmäßig 64 Multiplikationen und 48 Additionen. Mit SIMD benötigen wir jedoch nur mehr 16 Multiplikationen und 12 Additionen. Dadurch ersparen wir uns sehr viel Zeit in der Berechnung für diese Matrix*Matrix Funktion.

Unter den Links ganz unten, findest du ein Tutorial für SIMD: 'An Optimized Matrix Library'.

Assembler Optimierungen

Unter Visual C++ (6.0) kann man sich den Assemblercode, den der Compiler erstellt, ausgeben lassen. Dadurch kann man untersuchen, wie genau und gut der Compiler arbeitet. Selbstgeschriebener Assemblercode ist immer schneller als jeder Compiler. Compiler können nicht so gut sein, dass sie das händische nacharbeiten ersetzen. Unter 'Optionen' -> 'Einstellungen' -> Reiter 'C/C++' kann man unter 'Typ der Listing-Datei:' den Typ der Ausgabedatei einstellen. Hier können wir Kombinationen von Assembler-, Maschinen- und Quellcode auswählen. Für jede Quelldatei (*.cpp) wird jetzt eben diese Datei zusätzlich erstellt.

Links

    Jim Blinn's Corner: Optimizing C++ vector Expressions

    An Optimized Matrix Library in C++

    Cleaning Memory and Partial Register Stalls in Your Code

    GameDev: Performance Programming Applied to C++

    Flipcode Tutorial: Faster Vector Math Using Templates

Bücher

    Dov Bulka, David Mayhew: Efficient C++ Performance Programming Techniques. Addison Wesley, ISBN 0-201-37950-3

   

Copyright (c) by Kongo

Bei Fragen, Beschwerden, Wünschen E-mail an kongo@gmx.at

Tutorial vom 26.06.2001