Leistungsvervierfachung durch eine scheinbar nutzlose if-Anweisung

Leistungsvervierfachung durch eine scheinbar nutzlose if-Anweisung

CPUCompilerLeistungsoptimierungBranch PredictionLow-level-Prinzipien

Quellen:Lobsters + web research

Eine einzige zusätzliche Zeile im Code lässt ein Programm nicht langsamer, sondern viermal schneller laufen. Das klingt wie eine urbane Legende, doch am 12. Juli 2026 dokumentierte ein Programmierer namens purplesyringa in einem Blogbeitrag, dass ihm genau dieses Phänomen selbst widerfahren ist.

Er arbeitete damals an einem Datenkompressionsprogramm. Darin gab es eine sehr kurze Schleife mit nur einer Kernzeile – sie schlug wiederholt in einer Tabelle den nächsten Wert nach und speicherte das Gefundene. Eine einzige, saubere Anweisung. Doch das Programm lief frustrierend langsam. Er probierte verschiedene übliche Optimierungen aus, ohne nennenswerten Erfolg. Schließlich tat er etwas, das selbst ihm absurd vorkam: Er fügte eine völlig überflüssig wirkende if-Abfrage hinzu – er prüfte, 「ob der neu gefundene Wert dem aktuellen Wert entspricht」, und aktualisierte nur, wenn sie sich unterschieden, anderenfalls übersprang er die Aktualisierung.

Der Grad der 「Nutztlosigkeit」 dieser if-Zeile lässt sich etwa so veranschaulichen: Sie wissen bereits, dass sich hundert Yuan in Ihrer Tasche befinden, und doch greifen Sie hinein, um sich zu vergewissern, dass sie wirklich da sind, bevor Sie das Haus verlassen. Ob Sie die Hand ausstrecken oder nicht – in der Tasche bleiben es hundert Yuan. Doch das Erstaunliche: Nach dem Hinzufügen lief das Programm statt in 320 Mikrosekunden in nur noch 80 Mikrosekunden – exakt viermal schneller.

Als der Verfasser diesen Fall zum ersten Mal las, hielt auch er ihn für einen Scherz. Doch es ist keine schwarze Magie. Dahinter verbirgt sich eine Geschichte darüber, wie moderne Computer Ergebnisse 「erraten」.

Der Flaschenhals in der Fertigungsstraße

Um diese Geschichte zu verstehen, muss man zunächst wissen, wie eine CPU arbeitet.

Stellen Sie sich die CPU als eine Fabrik-Fertigungsstraße vor. Die Arbeiter an der Straße warten nicht, bis das vorherige Produkt vollständig montiert ist, bevor sie mit dem nächsten beginnen – das wäre viel zu langsam. Sie zerlegen die Arbeit in viele kleine Schritte: Zuschneiden, Schleifen, Lackieren, Qualitätskontrolle … Jede Station bearbeitet gleichzeitig ein anderes Produkt. Auf diese Weise hängt der Durchsatz der gesamten Straße von 「der langsamsten Station」 ab und nicht davon, 「ein Produkt nach dem anderen fertigzustellen」. Genau das ist die Befehlsebene-Parallelität (Instruction-Level Parallelism) moderner CPUs – mehrere Befehle gleichzeitig zu verarbeiten, um die Effizienz drastisch zu steigern.

Doch die Fertigungsstraße hat eine tödliche Schwäche: Wenn das nächste Produkt davon abhängt, was erst nach der Bearbeitung des vorherigen feststeht, gerät die gesamte Straße ins Stocken. Die Arbeiter müssen tatenlos warten.

In purplesyringas Code lag genau dieser Fall vor. Seine Schleife lautete: j = next_j[i][j] – mit dem aktuellen Wert j wurde die Tabelle nach dem nächsten j abgefragt, und dieser neue j ging in die nächste Runde der Abfrage ein. Jede Runde hing vom Ergebnis der vorherigen ab. Die Pipeline-Arbeiter in der CPU warteten ängstlich auf die Auslieferung der vorigen Station, während diese wiederum auf die davor wartete … Die ganze Straße wurde zu einem Stau auf einer Einbahnstraße. Das ist der Latenz-Flaschenhals, den man als 「Datenabhängigkeitskette」 bezeichnet.

Ein Navigationssystem, das 「den Weg rät」

Doch moderne CPUs verfügen über eine besondere Fähigkeit, die genau für diese Situationen gedacht ist. Sie heißt Branch Prediction (Branch Predictor).

Bleiben wir beim Fabrik-Beispiel: An der Fertigungsstraße gibt es eine Qualitätskontroll-Station, an der der Arbeiter je nach Prüfergebnis entscheidet, ob das Produkt in Kanal A oder Kanal B geleitet wird. Würde er bei jeder Station erst die Prüfung abwarten, bevor er den Kanal wählt, bliebe die Straße erneut stehen. Also rüstete die Fabrik ein 「Erfahrungs-System」 aus – sobald diese Prüfstation erreicht ist, schätzt das System anhand der letzten 99 Entscheidungen: Mit hoher Wahrscheinlichkeit geht es diesmal wieder durch A. Der Arbeiter schiebt das Produkt im Voraus in Richtung Kanal A. Lag die Schätzung richtig, läuft die Straße nahtlos weiter; lag sie falsch, wird das bereits in Kanal A vorgeschobene Halbfertigprodukt zurückgeholt und der Weg über Kanal B neu begonnen.

Der Branch Predictor der CPU ist genau dieses System. Er zeichnet die bisherigen Entscheidungen des Programms an jeder 「Gabelung」 auf und nutzt eine komplexe Schaltung, um die nächste Richtung vorherzusagen. Die Trefferquote moderner CPU-Branch-Predictoren liegt üblicherweise bei über 95 % – höher als die Entscheidungsgenauigkeit der meisten Menschen.

purplesyringas Erkenntnis war: Sein Code enthielt zwar keine offensichtliche 「Gabelung」 (kein if-else), doch die Datenabhängigkeitskette an sich war ein unsichtbares 「Warten」. Ihm kam der Einfall: Was, wenn man eine explizite Gabelung einführt und den Branch Predictor einschaltet?

Die eigentliche Wirkung dieser 「nutzlosen」 Zeile

Die von ihm hinzugefügte if-Bedingung lautete sinngemäß: Prüfe, ob das Tabellenergebnis vom aktuellen Wert abweicht; ist es gleich, tue nichts, ist es verschieden, aktualisiere erst. Da in den allermeisten Fällen der gefundene Wert tatsächlich mit dem aktuellen übereinstimmt, 「lernte」 der Branch Predictor der CPU schnell: Der Körper dieses if wird so gut wie nie ausgeführt.

Also wagte die CPU die kühne Annahme: Auch in der nächsten Runde wird der if-Körper übersprungen. Da sie das Überspringen annimmt, muss sie nicht auf das Ergebnis der vorigen Runde warten – sie setzt einfach voraus, dass sich j nicht geändert hat, und läuft weiter. Die Pipeline setzt sich wieder in Bewegung. Mehrere Schleifenrunden können parallel verarbeitet werden.

Wenn es selten doch einmal zu einem abweichenden Tabellenergebnis kommt, bemerkt der Branch Predictor seinen Fehler, verwirft das bereits falsch geleitete Halbfertigprodukt und führt diese Runde mit dem korrekten j-Wert neu aus. Dieser Vorgang heißt Branch Misprediction Penalty (Branch-Prediction-Fehlerstrafe). Doch weil der Anteil der Fehlvorhersagen extrem gering ist, wiegt dieser Preis weit weniger schwer als das ständige Warten über die gesamte Laufzeit.

Das Ergebnis: Eine völlig überflüssig wirkende if-Anweisung gab dem Branch Predictor ein Signal 「zum Raten」. Sie verwandelte eine Abhängigkeitskette, die eigentlich nur seriell ausgeführt werden konnte, in eine Pipeline, die spekulativ parallel ablaufen kann.

Wenn der Compiler mit 「guten Absichten」 Schaden anrichtet

Die Geschichte ist damit erst zur Hälfte erzählt. Es gibt einen noch lästigeren Gegenspieler: den Compiler.

Ein Compiler ist das Programm, das den vom Programmierer geschriebenen, menschenlesbaren Code in die Maschinenbefehle übersetzt, die die CPU ausführen kann. Moderne Compiler sind extrem schlau – so schlau, dass sie 「nutzlosen Code」 automatisch erkennen und gleich ganz entfernen. In den Augen des Compilers besagte purplesyringas hinzugefügte if-Zeile sinngemäß: 「Aktualisiere A nur, wenn A ungleich A ist」 – was offenkundig Unsinn ist. Der Compiler lachte darüber und optimierte sie weg.

Der Programmierer wollte den Branch Predictor der CPU übertölpeln, doch der Compiler beschlagnahmte das Täuschungsmittel als Erster.

Genau das ist die Bedeutung des Wortes 「konservative Entscheidung」 im Titel – und jener Punkt, den der Verfasser an diesem Fall am interessantesten findet: Der Compiler hält strikt am Grundsatz fest, 「die Programmsemantik nicht zu verändern」 – was Sie schreiben, ist logisch nutzlos, also übersetze ich es nicht. Was der Compiler jedoch nicht weiß: Der wahre Wert manchen Codes liegt auf der Hardware-Ebene – er liefert der CPU ein Signal, das eine spekulative Ausführung ermöglicht.

Eigentlich ist dies ein dreiseitiges Spiel. Die CPU ist aggressiv: Sie rät wild drauflos und versucht, Arbeit vorzuziehen. Der Compiler ist konservativ: Er hält sich strikt an die Semantik und tut weder zu viel noch zu wenig. Und der Programmierer steht dazwischen – er möchte die Aggressivität der CPU nutzen und zugleich die Konservativität des Compilers austricksen.

Das Siegel 「Fass hier nicht an」

purplesyringas Lösung bestand darin, das C-Schlüsselwort volatile zu verwenden. Dieses Wort wirkt in C wie ein Siegel 「Fass hier nicht an」, das dem Compiler aufgedrückt wird – es teilt dem Compiler mit: Diese Daten können sich ändern, ohne dass du es weißt, also optimiere sie nicht weg, sondern lies sie jedes Mal brav neu.

Nachdem dieses Siegel aufgedrückt war, hörte der Compiler auf, die if-Bedingung für 「nie erfüllbar」 zu halten, und behielt sie bei. Die if blieb erhalten, der Branch Predictor hatte wieder etwas zu raten, und die Pipeline konnte parallel weiterlaufen.

Später stellte in der Lobsters-Diskussion ein anderer Programmierer namens ibookstein fest, dass auch die C++20-Annotation [[unlikely]] (die dem Compiler sinngemäß sagt: 「Dieser Zweig wird selten genommen」) einen ähnlichen Effekt erzielt. purplesyringa wies jedoch darauf hin, dass die volatile-Siegel-Variante qualitativ besseren Maschinencode erzeugt und zudem nicht an einen bestimmten Compiler gebunden ist.

Ein größeres Konzept: Value Speculation

In dem Lobsters-Thread wies jemand darauf hin, dass dieser Trick eigentlich einen formellen Namen hat – Value Speculation (Wert-Spekulation). Die Kernidee lautet: Wenn wir für den Wert einer Variablen eine Heuristik besitzen, die 「mit hoher Wahrscheinlichkeit richtig rät」, können wir den Branch Predictor für eine spekulative Ausführung nutzen und so die Datenabhängigkeitskette durchbrechen.

Dieses Konzept lässt sich bis auf frühere Forschungsarbeiten und Blogs zurückverfolgen (die Arbeiten von Paul Khuong, Per Vognsen und anderen). In einem klassischen Artikel auf mazzo.li wird dieselbe Technik genutzt, um die Traversierung einer verketteten Liste zu beschleunigen: Beim Durchlaufen einer Linked List hängt die Adresse des nächsten Knotens vom im aktuellen Knoten gespeicherten Zeiger ab – ebenfalls eine Datenabhängigkeitskette. Wenn wir jedoch vermuten, 「der nächste Knoten liegt im Speicher direkt neben dem aktuellen」, kann die CPU vorab prefetchen und den Durchsatz von 14 GB/s auf 45 GB/s steigern (sofern sich die Daten im CPU-Cache befinden).

purplesyringas if-Trick und die Value Speculation sind im Kern dasselbe: Eine billige Vermutung ersetzt das teure Warten.

Was Ihnen im Weg steht

Das Interessanteste an dieser Geschichte ist, dass sie drei Ebenen des Konflikts zwischen 「Sie glauben, es sei so」 und 「es ist tatsächlich so」 aufdeckt:

Erste Ebene: Die menschliche Intuition besagt, 「je weniger Code, desto schneller läuft es」. In diesem Fall wurde das Programm durch eine zusätzliche Zeile jedoch schneller – denn die Funktion dieser Zeile bestand darin, ein Signal zu senden, nicht eine Berechnung auszuführen.

Zweite Ebene: Der Compiler meint, 「logisch nutzloser Code sollte entfernt werden」. Doch der Nutzen manchen Codes verbirgt sich auf der Ebene des Hardware-Verhaltens, nicht auf der Ebene der logischen Semantik.

Dritte Ebene: Wir neigen gemeinhin zu der Ansicht, 「wer falsch rät, zahlt einen Preis, also sollte man lieber gar nicht raten」. Die Design-Philosophie moderner CPUs ist jedoch genau umgekehrt: Rate mutig, bei Treffer gewinnst du, bei Fehler beginnst du eben neu. Solange die Trefferwahrscheinlichkeit hoch genug ist, ist die Bilanz insgesamt positiv.

Diese Geschichte hat kein großes, episches Ende. Es ist lediglich ein Programmierer, der bei der Optimierung eines Kompressionsalgorithmus zufällig auf eine kontraintuitive Tatsache stieß. Doch durch diese kleine if-Anweisung hindurch lässt sich eine feine Wahrheit aus der Tiefe moderner Computer erkennen: Die CPU ist ein Spieler, der Compiler ist ein Anwalt, und die besten Programmierer sind jene, die wissen, wann man den Anwalt übertölpelt und dem Spieler die Information zuspielt.


Quellen

  • Purplesyringa-Blog: Quadrupling code performance with a “useless” if (12. Juli 2026, der Originalartikel dokumentiert die vollständigen technischen Details, Codebeispiele und Leistungsdaten)
  • Lobsters-Community-Diskussion (s/1an425): 104 Punkte, 14 Kommentare, enthält ibooksteins Entdeckung der [[unlikely]]-Alternative sowie mikejsavages Hinweis auf das Konzept der Value Speculation mit Link
  • mazzo.li: Beating the L1 cache with value speculation (Juli 2021, detaillierte Erläuterung der Anwendung von Value Speculation bei der Linked-List-Traversierung, inklusive Leistungs-Vergleichsdiagrammen)