Moderne CPUs verfügen über eine Vielzahl an Möglichkeiten, ein Programm zu beschleunigen. Das bedeutet aber gleichzeitig, dass die Zahl von Fußangeln, die eine optimale Performance verhindern, wächst. Oft zeigt sich erst zur Laufzeit, wie ein Programm auf die gestellte Last reagiert. Damit kommt der Software-Optimierung ein immer größerer Stellenwert zu. Der Entwickler muss sich jedoch auf die verfügbare Hardware und ihre Möglichkeiten einstellen. Der Intel Vtune Performance Analyzer [1] erleichtert das Auffinden solcher Probleme. Dieser Artikel demonstriert dies anhand eines einfachen Beispiels.
Prozessor-Interna
Bevor ein Programm zu Ausführung gelangt, wird es in mehreren Schritten vom Sourcecode in Maschinensprache übersetzt. Es ist dabei unerheblich, ob das bei der Programmentwicklung durch Compiler oder erst zur Laufzeit durch Interpreter geschieht - die CPU sieht am Ende nur noch den binär kodierten Assembler-Code.
Jede Instruktion besteht aus einem oder mehreren Bytes Code, der für die CPU eine elementare Operation darstellt, zum Beispiel: Lade aus der Adresse 0x1234 das Datum in das Register RAX. Intern verarbeiten moderne CPUs eine solche Anweisung nicht direkt, sondern in kleinere Mikro-Operationen - so genannte µops - aufgeteilt. Diese Vorgangsweise führt aber auch dazu, dass Befehle nicht mehr in einem Takt abgearbeitet werden, sondern mindestens in diesen Stufen:
- Lade einen Befehl.
- Zerlege den Befehl in µops.
-
Führe den/die µops aus, berechne notwendige
Adressen.
- Optional: Lade die Daten.
- Schreibe das Ergebnis in den Speicher.
Im realen Prozessor-Design geht die Unterteilung noch weiter, sodass sich am Ende oft zehn oder mehr Schritte ergeben - bei machen Systemen sind es über 30. Statt einen Assembler-Befehl nach dem anderen abzuarbeiten, wandert, sobald ein Befehl die erste Stufe passiert hat, der Folgebefehl in die Pipeline.
Unter optimalen Bedingungen könnte rein serieller Code nach diesem Verfahren mit maximaler Performance ablaufen. In der Praxis ist das aber selten möglich: Es treten wegen der Verzweigungen und Schleifen im Quellprogramm immer wieder Sprünge auf. Statt dann einfach zu warten, bis das Ziel eines bedingten Sprungs bekannt ist, versucht die Voraussage-Logik der CPU den wahrscheinlichsten Pfad zu erkennen.
Erkennt das System am Ende der Pipeline, dass die Vorhersage falsch war, muss es alle anderen in der Pipeline gerade aktiven Tätigkeiten abbrechen und die Pipeline neu füllen. Diese so genannte Branch-Misprediction kostet verständlicherweise sehr viel Zeit.
Caches
Ein anderer häufig auftretender Bremsklotz ist der Zugriff der CPU auf den Hauptspeicher. Aktuelle CPUs werden mit Taktfrequenzen von 2 bis 4 GHz betrieben, ein Takt dauert also 0,5 Nanosekunden. Muss die CPU auf den Hauptspeicher zugreifen, entstehen Wartezeiten von 70 bis 100 ns, also bis zu 600 Takte. Während dieser Zeit kann der Prozessor im Allgemeinen keine sinnvolle Arbeit verrichten. Dieser Problematik sollen Caches entgegenwirken, die Zugriffszeiten durch schnelleren Speicher minimieren. Sie sind heute aber wesentlich kleiner als die realen Programme oder Datensätze. Das Programm muss also auch so gestaltet sein, dass die Caches optimal verwendet werden.
Neben diesen beiden Beispielen gibt es noch viele weitere Möglichkeiten für eine Optimierung. Glücklicherweise hilft moderne Hardware dem Programmierer dabei weiter. Die auf Intel Pentium oder Core 2 Duo basierenden Prozessoren können das Auftreten so genannter Events registrieren. In jedem Prozessor gibt es einige Zählregister, deren Wert bei jedem Auftreten des gemessenen Events inkrementiert wird. Überschreitet ein Zähler eine zuvor festgelegte Schranke - in der Intel Terminologie als SAV (Sampling after Value) bezeichnet -, löst die CPU einen Interrupt aus. Geeignete Software, also ein Treiber im Betriebssystem, kann auf den Interrupt reagieren und den Event auswerten.