Performance Tuning

Regelmäßig führe ich die folgende Diskussion mit Entwicklern:

Ich: „Wenn man hier noch eine polymorphe Indirektion/lokale Variable/Methodenaufruf einzieht, wird das ganz deutlich besser testbar/lesbar/änderbar.“
Entwickler: „Oh nein, dadurch wird die Peformance zu schlecht, den Code können wir nicht ändern.“

Diese Entgegnung gründet meistens in einem sehr begrenzten Verständnis von dem, was moderne Compiler, Laufzeitumgebungen und Prozessoren leisten. Aber dazu komme ich gleich.

Aus meiner Erfahrung gibt es drei Gründe für schlechte Performance mit absteigender Bedeutung:

  1. Schlechtes Design, aus dem inperformante Datenbankzugriffe entstehen. Datenbanken sind sehr gut, wenn es darum geht, aus einer große Grundmenge schnell nach bestimmten Kriterien zu filtern. Sie sind aber langsam, wenn es darum geht, durch ein Geflecht von Objekten zu navigieren. Verliert ein Team durch schlechtes Design den Überblick über den Code, kommt es häufig dazu, dass Funktionalität, die in der Programmiersprache sehr schnell wäre, durch komplexe und langsame Datenbankoperationen abgebildet wird und umgekehrt langsame Suchoperationen im Speicher gemacht werden, für die die Datenbank besser geeignet ist. Um das abzustellen, muss man allerdings den Code oft ganz wesentlich umbauen. Andererseits habe ich durch solche Umbauten Systeme schon um einen Faktor 1000 schneller machen können. Ein Faktor 10 bis 100 ist fast immer machbar.
  2. Schlechtes Design, bei dem das Team die Übersicht verloren hat. Dies führt dazu, dass aufwändige Operationen immer wieder durchgeführt werden. Je nach Fachlichkeit kommt es bei diesen Problemen auch vor, dass entsprechende Umbauten das System um den Faktor 10 beschleunigt.
  3. Schlechtes Design, bei dem ungeeignete oder zu einfache Algorithmen und Datenstrukturen verwendet werden. Häufig tritt dieses Problem auf, wenn anstatt geeigneter Objektstrukturen und Bibliotheksklassen Arrays und Indexschleifen verwendet werden. Dies führt oft dazu, dass selbst einfache Algorithmen so komplex zu programmieren sind, dass an den Einsatz leistungsfähigerer Varianten gar nicht erst gedacht wurde. Ironischer weise erhalte ich auf die Frage, warum man hier keinen Vector oder eine Map eingesetzt habe oft die Antwort „Die sind ja viel langsamer, als ein Array“. Nicht selten führt der Umbau dann zunächst zu einer deutlichen Beschleunigung und eröffnet durch den übersichtlicheren Code auf einmal deutlich bessere und auch schnellere Alternativen.

Nun ist es nicht immer sinnvoll, die schnellste Variante zu wählen. Code, der nur einmal durchlaufen wird, ist weniger kritisch, als der Kern des Systems. Grundsätzlich gilt: Erst zum laufen bringen, dann messen und dann erst optimieren. Zum Messen empfehlen sich professionelle Profiler wie z.B. Rational Quantify, die sehr gute Möglichkeiten bieten, kritische Stellen im Code zu identifizieren und seine Energien dort einzusetzen, wo man echte Hebelwirkung hat.

Und was ist mit den Low-Level Optimierungen in der Programmiersprache? Anders als alte Assemblermakros führen moderne Compiler hochgradige Optimierungen durch, die weit über das hinausgehen, was man manuell noch beherrschen könnte; Prozessoren parallelisieren verschiedenste Operationen, so dass Indirektionen und Variablenzugriffe zum Teil höchstens noch einen zusätzlichen Zyklus (also 0,2 Milliardenstel Sekunden) brauchen, oft sogar kostenneutral sind; Virtuelle Maschinen wie die Java VM gehen sogar noch weiter und überwachen die Abläufe zur Laufzeit, um dann den Code bei Bedarf nach neuen Kriterien zu optimieren. Sie wählen unter verschiedenen Optimierungsvarianten die aus, die am besten zum aktuellen Laufzeitverhalten passt. Der Versuch, diese Mechanismen durch „geschickte Programmierung“ zu „unterstützen“ geht in mehr als 90% der Fälle nach hinten los: Die zusätzliche Komplexität hebelt den Optimierer aus, der Code wird langsamer, als vorher. Von der Gefahr, den Überblick über das Design zu verlieren und sich ernsthafte Performanceprobleme einzuziehen ganz zu schweigen.

Fast alle Teams, denen ich bisher bei der Optimierung dieses Codes geholfen habe, waren zuvor in diese Falle geraten. Zumindest bei betrieblichen Informationssystemen ist eine Optimierung auf unterster Ebene praktisch immer sinnlos und kontraproduktiv.