Ifs nach oben, Fors nach unten verschieben
(matklad.github.io)- Wenn man if-Anweisungen innerhalb einer Funktion an die Aufrufstelle hochzieht, hilft das, die Komplexität des Codes zu verringern
- Wenn Bedingungsprüfungen und Verzweigungslogik an einer Stelle gebündelt werden, lassen sich Duplikate und unnötige Verzweigungsprüfungen leichter erkennen
- Mit dem Refactoring zum Auflösen von Enums lässt sich verhindern, dass dieselbe Bedingung an vielen Stellen im Code verstreut ist
- Auf Batch-Verarbeitung basierende
for-Schleifen sind wirksam für Leistungssteigerungen und die Optimierung wiederholter Arbeit - Durch die Kombination des Musters ifs nach oben, fors nach unten lassen sich Lesbarkeit und Effizienz des Codes zugleich steigern
Kurze Notiz zu zwei zusammenhängenden Regeln
- Wenn es innerhalb einer Funktion eine if-Bedingung gibt, empfiehlt es sich zu prüfen, ob sie an die Aufrufstelle verschoben werden kann
- Wie im Beispiel ist es wünschenswerter, statt Vorbedingungen innerhalb der Funktion zu prüfen, die entsprechende Prüfung an die Aufrufstelle zu verlagern oder die Vorbedingung per Typ (oder
assert) zu garantieren - Das Hochziehen von Vorbedingungen (Push up) wirkt sich auf den gesamten Code aus und reduziert insgesamt die Anzahl unnötiger Bedingungsprüfungen
Kontrollfluss und Bündelung von Bedingungen
- Kontrollfluss und if-Anweisungen sind wesentliche Ursachen für Komplexität und Bugs im Code
- Es ist nützlich, Bedingungen auf höhere Ebenen wie die Aufrufstelle zu konzentrieren, die Verzweigungslogik in einer Funktion zu bündeln und die eigentliche Arbeit geradlinigen Subroutinen zu überlassen
- Wenn Verzweigungen und Kontrollfluss an einer Stelle zusammenkommen, lassen sich duplizierte Verzweigungen und unnötige Bedingungen leicht erkennen
Beispiele:
- Wenn in Funktion
fverschachtelteif-Anweisungen stehen, ist toter Code (Dead Branch) leichter zu erkennen - Wenn Verzweigungen über mehrere Funktionen (
g,h) verteilt sind, wird das schwieriger
Refactoring zum Auflösen von Enums (Dissolving enum Refactor)
- Wenn der Code dieselbe Verzweigungslogik etwa in einem Enum verkapselt, kann man die Bedingung nach oben ziehen und so Verzweigung und Arbeit klarer voneinander trennen
- Mit diesem Ansatz lässt sich verhindern, dass dieselbe Bedingung mehrfach im Code wiederholt auftaucht
Beispiel:
- Eine Situation, in der dieselbe Verzweigungsbedingung jeweils in den Funktionen
f,gund im EnumEausgedrückt wird, - lässt sich durch eine einzige übergeordnete Verzweigung für den gesamten Code vereinfachen
Datenorientiertes Denken (Data Oriented Thinking) und Batch-Verarbeitung
- Die meisten Programme arbeiten mit vielen Objekten (Entitäten). Auf dem kritischen Pfad (Hot Path) wird die Performance durch die Verarbeitung vieler Objekte bestimmt
- Es ist sinnvoll, das Konzept von Batches einzuführen und Operationen auf Mengen von Objekten zum Standard zu machen, während Operationen auf Einzelobjekten als Sonderfall behandelt werden
Beispiele:
-
Mit einer Funktion wie
frobnicate_batch(walruses)dient Batch-Verarbeitung als Standard, -
und einzelne Objekte können als Sonderfall über eine
for-Schleife verarbeitet werden -
Dieser Ansatz spielt bei der Performance-Optimierung eine wichtige Rolle, weil er bei großen Arbeitsmengen Startkosten senkt und mehr Flexibilität bei der Reihenfolge ermöglicht
-
Auch der Einsatz von SIMD-Operationen (etwa struct-of-array) ist möglich, sodass zunächst nur bestimmte Felder gesammelt verarbeitet und danach die Gesamtarbeit ausgeführt werden kann
Praktische Beispiele und empfohlene Muster
- Wie bei FFT-basierter Polynom-Multiplikation kann man die Performance maximieren, indem Berechnungen an mehreren Punkten gleichzeitig ausgeführt werden
- Die Regel, Bedingungen nach oben und Schleifen nach unten zu verlagern, lässt sich parallel anwenden
Beispiele:
- Statt innerhalb einer Schleife immer wieder dieselbe Bedingung zu prüfen, kann man die Bedingung aus der Schleife herausziehen; das reduziert Verzweigungen im Loop und erleichtert Optimierung und Vektorisierung
- Dieser Ansatz gewährleistet auch in Datenebenen großer Systeme wie dem Design von TigerBeetle hohe Effizienz
Fazit
- Durch die Kombination des Musters, if-Anweisungen (Bedingungen) nach oben (Aufrufstelle, Steuerungsebene) und for-Schleifen (Wiederholungen) nach unten (Ausführungsebene, Datenverarbeitung) zu verlagern, lassen sich Lesbarkeit, Effizienz und Performance des Codes verbessern
- Das Denken aus der Perspektive abstrakter Vektorräume, also Operationen auf Mengenebene, ist ein besseres Werkzeug zur Problemlösung als wiederholte Verzweigungsverarbeitung
- Kurz gesagt: if nach oben, for nach unten!
1 Kommentare
Hacker-News-Kommentare
getaddrinfooderEnterCriticalSectionverlangen? Ich denke, so eine Umformung kommt nur dann infrage, wenn etwas vielleicht an nur zwei Stellen aufgerufen wird und die Entscheidung außerhalb des Zuständigkeitsbereichs der Funktion liegt. Ein Ansatz ist, eine Funktion, die nur Bedingungen ausführt, die eigentliche Arbeit an eine Helper-Funktion delegieren zu lassen. Und wenn man eine Bedingung aus einer Schleife herausziehen muss, kann man den Aufrufer direkt den niedrigeren Condition-Helper verwenden lassen. Der Kern dieser Überlegung ist aber „Optimierung“. Optimierung steht oft im Konflikt mit besserem Programmdesign. Es kann das bessere Design sein, wenn der Aufrufer die Bedingung gar nicht kennen muss. Dieses Dilemma taucht in OOP häufig auf. Die Entscheidung, die durch „if“ repräsentiert wird, erfolgt in Wirklichkeit oft über Method Dispatch. Auch dieses Dispatching aus der Schleife herauszuziehen, kann mit Designprinzipien kollidieren. Ein Beispiel wäre, beim Zeichnen eines Bildes auf ein Canvas statt wiederholtputpixelaufzurufen lieber eine Methode wieblitzu verwendenEnterCriticalSectionist es richtig, am Eintrittspunkt starke Validierung durchzuführen, einschließlich Bedingungen. In Anwendungscode kann man das if aber durchaus zum Aufrufer verschieben. In Libraries oder Kerncode ist es passend, den Kontrollfluss an den Rand zu verlagern. Innerhalb der eigenen Domäne ist es gut, den Kontrollfluss an den Rändern zu halten. Aber solche Regeln sind immer nur idiomatisch; jemand, der vernünftig urteilen kann, muss sie dem Kontext entsprechend anwendenmatch-Anweisung kann durch polymorphe Methodenaufrufe ersetzt werden. Ziel dieses Ansatzes ist es, den Zeitpunkt der anfänglichen Fallunterscheidung vom Zeitpunkt der eigentlichen Ausführung des Verhaltens zu trennen. Die Fallunterscheidung steckt im Objekt, hier also im Enum-Wert, oder in der Closure, daher muss man sie nicht bei jedem Aufruf wiederholen. Wenn sich die Fallunterscheidung ändert, muss man nur den Verzweigungspunkt anpassen; die Stellen, an denen das tatsächliche Verhalten stattfindet, müssen nicht verändert werden. Der Nachteil ist der Trade-off zwischen dem Komfort, die Verhaltensverzweigungen pro Fall direkt prüfen zu können, und der Tatsache, dass auf Codeebene eine Abhängigkeit von der Fallliste entstehtif (weShouldDoThis()) { doThis(); }in dieser Art; wenn man jede Prüfung in eine eigene Funktion auslagert, werden Tests und Komplexitätsmanagement einfachersonarqubeund Ähnliches melden wahllos auch „code smells“, die gar keine echten Bugs sind. Wenn man dann anfängt, sogar „problemfreien Code“ umzubauen, steigt nur das Risiko, dabei neue Bugs einzuführen, und man verschwendet Zeit, die man für wirklich wichtige Probleme bräuchtefor-Schleife leichter zu handhaben und speichereffizienter als ein if außerhalb derfor-Schleife. Ein konkretes Beispiel: Wenn bei ungeraden Werten+1und bei geraden-2gerechnet werden soll, müsste man ursprünglich in jeder Schleifeniteration verzweigen, aber mit SIMD kann man alle 16intgleichzeitig verarbeiten, ganz ohne Branch. Wenn der Compiler korrekt vektorisiert, kann er den ursprünglichen Code in eine branchlose optimierte Version umwandelnfor-Schleife eine datenabhängige Verzweigung und lässt sich nicht einfach nach oben ziehen. Wenn der Algorithmus stattdessen eine Struktur wieif (length % 2 == 1) { ... } else { ... }mit einer Bedingung außerhalb der Schleife hätte, dann wäre es natürlich richtig, diese Bedingung vor diefor-Schleife zu ziehen. In der SIMD-Version verschwindet das if ganz, und genau so ein Code-Muster ist ideal; vermutlich würde auch der Autor des Artikels das mögenfor-Schleife verzweigt. Weiß jemand, wie schwierig es für Compiler ist, solchen Code automatisch zu vektorisieren? Mich interessiert, wo da die Grenze liegtfor-Schleife erzeugen die Abstraktion „for ist die Regel, Verzweigung ist das Verhalten“. Wenn dann neue Anforderungen auftauchen, bricht diese Abstraktion auseinander, und man stopft gewaltsam Parameter hinein oder fügt immer mehr Ausnahmebehandlung hinzu, wodurch der Code schwer verständlich wird. Hätte man von Anfang an ohne diese Abstraktion geschrieben, wäre das Ergebnis wahrscheinlich klarer und leichter wartbar gewesen. https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction