Über Premature Optimization
Premature optimization is the root of all evil.
Diese plakative Aussage findet sich in dem 1974 erschienenen Aufsatz Structured Programming with go to Statements
von Donald E. Knuth. Damit beschreibt er seine Beobachtung, dass Entwickler viel Zeit mit der Optimierung der Laufzeiteffizienz von Programmteilen verbringen, die nicht auf dem kritischen Pfad liegen, und dass sich diese Optimierungsversuche besonders negativ auf den Aufwand für Fehlersuche und Wartung auswirken.
Zwei typische Beispiele: Komplexe Datenstrukturen, die zwar effizienter, für den konkreten Anwendungsfall allerdings unnötig sind. Sowie Mikro-Optimierungen, die zu Lasten ungewöhnlicher Kontroll- und Datenflüsse ein wenig Performance gewinnen. Einfach formuliert definiert sich also Premature Optimization durch die folgenden beiden Eigenschaften:
- deutliche Zusatzaufwände unter Berücksichtigung der Weiterentwicklung und Wartung,
- irrelevanter oder fraglicher Nutzen der höheren Effizienz.
Wichtig für die Definition ist, dass beide Eigenschaften erfüllt sind. Denn falls die Optimierung für den Erfolg der Anwendung notwendig ist, handelt es sich nicht um Premature Optimization
. Analog gilt das für Maßnahmen, die keine zusätzlichen Aufwände verursachen.
40 Jahre später
Seit der Veröffentlichung von Donald E. Knuth sind fast 40 Jahre vergangen, und Premature Optimization
gehört heute gefühlt nicht mehr zu den größten Problemen der Softwareentwicklung. Dazu haben aus meiner Sicht mehrere Dinge beigetragen. Natürlich gehört dazu, dass das Problem überhaupt benannt und diskutiert worden ist. Mindestens genauso wichtig finde ich allerdings die folgenden beiden Punkte:
- Die meisten Programmiersprachen kommen heutzutage zusammen mit einer umfangreichen Bibliothek, die zahlreiche hochwertige und wiederverwendbare Datenstrukturen enthält. Dadurch hat das Bedürfnis der Entwickler nach eigenen Implementierungen stark nachgelassen.
- Die modernen Compiler und Laufzeitumgebungen führen selbst umfangreiche Mikro-Optimierungen durch, mit denen die meisten Entwickler aufgrund der gestiegenen Hardware-Komplexität kaum noch mithalten können.
Ich kenne das aus eigener Erfahrung: Vor 20 Jahren habe ich noch alle Nase lang verkettete Listen und balancierte Bäume implementiert. Heutzutage gibt es keinen Bedarf mehr dafür – nur die Laufzeitkomplexität sollte man natürlich kennen.
Ein neues Problem
In Zusammenhang zur Premature Optimization
ist in den letzten zehn Jahren allerdings ein ganz anderes Problem entstanden, das sich entweder durch gefühlt schlechtes Antwortverhalten oder durch schlechte Energieeffizienz manifestiert. Am einfachsten lässt es sich an einem typischen Fall erklären:
Häufig sieht man in Java eine LinkedList
an Stelle einer ArrayList
, ohne dass es dafür einen ersichtlichen Grund gibt. Dabei ist Erstere in solchen Situationen Letzteren in fast jeder Hinsicht unterlegen. Die falsche Verwendung ist in der Regel auf Unkenntnis zurückzuführen. Bedenklich ist dabei vor allem, dass der Code nicht korrigiert wird. Denn diese Versuche werden häufig als Premature Optimization
abgetan. Da die Verwendung passender Datenstrukturen keinen zusätzlichen Aufwand verursachen, ist es aber genau das nicht. Sehr wohl wirkt es sich aber stark negativ auf die Effizienz aus.
Premature Pessimization
Diese Entwicklung ist ein immer größer werdendes Problem. Mittlerweile hat sich dafür der Begriff Premature Pessimization
etabliert. Er bezeichnet Code, der die gleiche Komplexität wie idiomatischer Code aufweist, gleichzeitig jedoch deutlich ineffizienter ist – unabhängig vom Nachweis des Nutzens für die gesamte Anwendung. Herb Sutter definiert den Begriff ähnlich: Premature pessimization is when you write code that is slower than it needs to be, usually by asking for unnecessary extra work, when equivalently complex code would be faster and should just naturally flow out of your fingers.
Neben der Wahl der passenden Datenstruktur gibt es weitere typische Beispiele: In C++ ist beispielsweise die Bevorzugung des Pre-Increment-Operators ++i
gegenüber dem Post-Increment-Operator i++
idiomatisch – genauso wie die Parameterübergabe per const
-Referenz, wenn die Argumente weder kopiert noch verändert werden.
Fazit
Meiner Erfahrung nach gibt es zwei unterschiedliche Arten von Performance-Problemen: Im einen Fall gibt es einen klaren Flaschenhals, wohingegen sich im anderen Fall die Laufzeit gleichmäßig auf viele Stellen verteilt. Ersteres kann vergleichsweise einfach mit klassischem Profiling identifiziert und eingegrenzt werden. Letzteres ist dagegen das Ergebnis üblicherweise das Ergebnis der Premature Pessimization
und ist durch konsequentes Refactoring zu beheben. Daher sollte man besser das unnötige Premature Pessimization
von Beginn an vermeiden.