Weg mit den Schleifen!

Seite 3: Einige Schleifen zum Dessert

Inhaltsverzeichnis

Die augenscheinlich besseren Alternativen werfen die Frage auf, ob es überhaupt Situationen gibt, in denen klassische Schleifen sinnvoll sind.

Zum einen benötigen Bibliotheksentwickler und Compilerbauer Schleifen zum Entwickeln der Higher-Order-Funktionen. Ein map lässt sich durch ein reduce ausdrücken, aber letztlich läuft spätestens auf Maschinensprachebene irgendwo eine klassische Schleife.

Vielleicht lässt sich die Frage anders formulieren: Gibt es für "normale" Anwendungsentwickler Situationen, in denen sie auf Schleifen zurückgreifen sollten?

Die Antwort lautet: Ja, die gibt es, und der Grund ist schlicht die oft bessere Performance.

Zunächst einmal sollte man sich aber in Erinnerung rufen, dass wie Donald Knuth sagte, verfrühte Optimierung die Wurzel allen Übels ist. Unter dem Deckmantel der Performance-Optimierung völlig unleserlichen Code zu schreiben, ist zum Glück aus der Mode gekommen. Statt den Code immer und überall übermäßig zu optimieren, gilt es, nach Bottelnecks zu suchen – also den wenigen Stellen im Code, die tatsächlich einen nennenswerten Einfluss auf die Ausführungsgeschwindigkeit oder andere Performance-Metriken haben.

Das trifft auch auf die Schleifenthematik zu. Sollte sich herausstellen, dass eine Rekursion oder eine Higher-Order-Funktion der Performance merklich schadet, kann eine klassische Schleife helfen. Dabei sollten Entwickler nicht raten, sondern messen (Benchmarking, Profiling), ob das wirklich der Fall ist.

Außerdem hilft das Verständnis, wie sich die Performance der unterschiedlichen Alternativen im Vergleich zur Schleife ausmisst.

Rekursionen schneiden traditionell mit einem schlechteren Performanceprofil ab, sowohl hinsichtlich der Laufzeitgeschwindigkeit als auch bezüglich des Speicherbedarfs. Das liegt daran, dass jeder rekursive Aufruf Daten auf den Stack legt und sie beim return wieder abräumen muss. Es gibt allerdings einen Weg, der Stack-Falle zu entrinnen: Tail Call Optimization (TCO) ist eine Optimierung, die der Compiler oder die VM anbieten kann, um den Stack zu umgehen. Dazu muss die Rekursion in "Tail-Position" vorliegen, das heißt, nach dem return dürfen keine weiteren Operationen erfolgen:

// Nicht in tail position
function x() {
...
return x(...) + 1;
}

// In tail position
function y() {
...
return y(...);
}

Unter anderem unterstützen Scheme, Erlang/Elixir, Scala und Kotlin TCO. Ruby und Python können die Optimierung mit Plug-ins erlernen. [Update]Leider hängt es bei zahlreichen verbreiteten Sprachen wie Java, C++ oder JavaScript vom eingesetzten Compiler oder der Engine/VM und der genauen Konfiguration wie Compilerflags ab, ob und unter welchen Umständen sie TCO beherrschen.[Update]

Die Performance von Higher-Order-Funktionen ist stark abhängig von der Programmiersprache beziehungsweise Implementierung, wodurch eine ausführliche Betrachtung unmöglich ist. Als Beispiel kann jedoch eine Performancebetrachtung von Angelika Langer zu Java-Streams dienen. Sie stellt beispielsweise fest, dass die Unterschiede zwischen reduce und einer Schleife bei ArrayList verschwindend gering ist. Auf Java-Int-Arrays dagegen operieren Schleifen um den Faktor 15 schneller. Andererseits lassen sich Streams leicht parallelisieren und können damit die Vorteile mehrerer CPU-Kerne nutzen, was mit klassischen Schleifen nicht direkt möglich ist. Im Endeffekt müssen Entwickler jede Situation anders beurteilen und vor allem messen.

"... don't guess; instead, benchmark a lot" - Angelika Langer.