Schwarm-Codereien und Code-Schwärmereien – Teil 2: Der Flug des Falken
Ein Jahr ist vergangen. Ein Jahr, in dem die digitalen Vögel für Transients unermüdlich ihre Kreise gezogen haben. Wer drei Wochen damit verbringt, einer Simulation das Fliegen beizubringen, sollte meinen, das System sei irgendwann fertig. Allerdings ist Software kein Gemälde; sie ist ein lebendiger Organismus. Und wer einmal den Kaninchenbau der GPU-Programmierung betreten hat, den lässt das leise Flüstern ungenutzter Taktzyklen nicht mehr los.
Seit den ersten Gehversuchen ist eine Menge passiert. Genauer: seit dem Tag, an dem ich den gesamten architektonischen Unterbau einriss und die Vögel aus ihrem goldenen, schweren Käfig befreite.
Das ist die Geschichte des zweiten Jahrs im Schwarm: von gefräßigen Albatrossen, dem lautlosen Kehren des Müllmanns und der Kunst, dem System nicht im Weg zu stehen.
I
Wer die Simulation als Desktop-App auf dem Mac laufen lassen wollte, bekam anfangs ein bewährtes, fettleibiges Paket geschnürt: Electron.
Electron ist der gutmütige Albatros der modernen Softwareentwicklung. Er fliegt stabil, allerdings trägt er einen kompletten Browser in sich, mit all seinem Inventar. Für meine Simulation bedeutete das: Eine Web-App, die eigentlich nur ein paar Megabyte groß sein sollte, wog als Desktop-App plötzlich über 530 Megabyte. Beim Starten krallte sie sich Arbeitsspeicher, als gäbe es kein Morgen. Ein Vogelschwarm, der mit dem Gewicht eines Jumbo-Jets abhebt.
Tauri wirft den Ballast über Bord. Statt eine eigene Browser-Engine mitzubringen, nutzt Tauri die bereits im Betriebssystem verankerten. Das Backend steuert Rust, eine Programmiersprache, die für ihre Effizienz und Speichersicherheit bekannt ist.
Die Wirkung war dramatisch: Die App schrumpfte von einem dreistelligen Megabyte-Koloss auf vier Megabyte. Der Speicherbedarf kollabierte auf einen Bruchteil. Der Albatros wich einem schnellen, schlanken Falken.
Und weil ich schon dabei war, packte mich der erste Ehrgeiz. Ich bin kein Programmierer, aber ich bin hartnäckig. Die erste große Herausforderung war die Titelleiste mit ihren Ampelknöpfen (Traffic Lights): Sie sollten sich elegant ausblenden und sich nahtlos an den Dark Mode anpassen. Was trivial klingt, ist es nicht, wenn man keine Ahnung von Rust hat. Perfektionismus ist vielleicht eine Krankheit, allerdings eine sehr hübsche.
II
Mit dem neuen, schlanken Gehäuse unter Tauri widmete ich mich dem Innenleben. Und stieß auf den lautlosen Killer jeder flüssigen 60-FPS-Simulation: den Garbage Collector.
In einem normalen Browser müssen sich Entwickler:innen selten um Speicherverwaltung kümmern. Wenn ein Objekt erstellt wird, reserviert die Engine den Speicher; wenn es nicht mehr gebraucht wird, räumt sie es irgendwann weg. Dieser automatische Müllmann arbeitet im Hintergrund, allerdings muss er, um den Müll zu sortieren, die Ausführung des Codes für winzige Sekundenbruchteile anhalten, Stop the World.
Bei einer Simulation mit 4096 Vögeln, die in einer engen Schleife ständig Abstände berechnen, ist das fatal. In meiner ursprünglichen Implementierung erstellte ich in jedem Durchlauf temporäre Vektoren und klonte sie fleißig.
Das sieht elegant aus, ist aber so, als würde man in einem Meeting für jeden Satz ein neues Blatt beschreiben, es zerknüllen und auf den Boden werfen. Nach wenigen Sekunden stehen wir knietief im Müll. Der Garbage Collector muss aufräumen, die Framerate gerät ins Stolpern, Mikroruckler ruinieren die Illusion des gleitenden Schwarms.
Die Lösung war eine strenge CPU-Diät. Ich verbannte alle Objekt-Instanziierungen aus der Schleife. Stattdessen nutze ich flache, wiederverwendbare Arrays und führe die Berechnungen direkt auf den nackten Zahlenwerten durch. Kein Klonen, keine neuen Objekte, kein Müll. Der Müllmann hat Feierabend, die Simulation läuft glatt.
Gleichzeitig nahm ich mir eine Funktion vor, die Daten flexibel entgegennimmt. In JavaScript bedeutet das, dass die Engine bei jedem Aufruf im Hintergrund ein neues Array anlegt. Bei tausenden Vertices pro Frame summiert sich das zu spürbarer Rechenzeit. Ich ersetzte sie durch explizite, feste Parameter. Das Benchmark-Ergebnis: 5,2-fache Beschleunigung der Geometrie-Generierung. Manchmal ist das Langweiligere das dramatisch Schnellere.
III
Wer 4096 Vögel zeichnen will, jeder aus 3 Dreiecken (9 Eckpunkten), schickte dem Grafikprozessor (GPU) bisher eine riesige Liste mit 36.864 Eckpunkten. Jeden Frame neu. Das ist ineffizient. Warum der GPU 4096-mal erklären, wie ein Vogel aussieht, wenn alle Vögel gleich aussehen?
Das geht besser, und das Prinzip ist bestechend simpel: Ich definiere ein einziges Mal die Geometrie eines einzigen Vogels, die neun Vertices. Dann sage ich der GPU: „Hier ist die Vorlage, und hier sind 4096 Positionen und Rotationen. Nimm sie und stemple 4096-mal auf den Bildschirm.“
Dieser Wechsel entlastete den Flaschenhals zwischen CPU und GPU massiv. Statt Tonnen von Geometriedaten über den Bus zu schaufeln, flüstere ich der Grafikkarte nur noch die Instanz-Koordinaten zu. Die Grafikkarte zeichnet alle Vögel in einem einzigen, hocheffizienten Rutsch, Single Draw Call.
IV
Das Meisterstück dieses Jahres verdanke ich einem architektonischen Dilemma, das mir schlaflose Nächte bereitete. Erinnern wir uns an das dynamische Farbsystem aus Teil 1: Vögel ändern ihre Farbe je nach ihrer strategischen Rolle (Strategic Leaders, Consultants, Change Champions). Diese Rollen basieren darauf, wer dem Ziel am nächsten ist oder wer besonders dynamisch fliegt.
Da die gesamte Physik der Vögel direkt auf der GPU in hochoptimierten Shadern berechnet wird, liegen ihre Positionen und Geschwindigkeiten im Videospeicher (VRAM) der Grafikkarte. Die CPU, mein JavaScript-Code, weiß davon zunächst nichts. Um die strategischen Rollen berechnen zu können, musste ich die Positionen von der GPU zurück zur CPU holen.
Der klassische Weg ist eine Katastrophe: ein synchroner Befehl, der die gesamte Anwendung blockiert. Ruft JavaScript diesen Befehl auf, friert der Thread ein und wartet, bis die GPU alle Zeichenbefehle abgearbeitet und die Daten mühsam zurückgeschaufelt hat. Dieser Pipeline Stall warf mir im 60-Hz-Takt Knüppel zwischen die Beine. Die Vögel flogen physikalisch schnell, allerdings stotterte die Visualisierung auf schwächeren Rechnern, sobald die Rollen neu berechnet wurden.
Die Rettung ist Asynchronizität. Statt die GPU anzubetteln: „Gib mir die Daten, und zwar jetzt sofort!“, sage ich nun: „Kopiere die Positionsdaten im Hintergrund in diesen Puffer. Ich frage in zwei Frames nach, ob du fertig bist.“
Die GPU führt diese Kopie schnell und unabhängig im Grafikspeicher aus. JavaScript läuft ungestört mit 60 FPS weiter. Zwei Frames später holen wir uns die Daten ohne Blockierung ab. Das Ergebnis: eine echtzeitbasierte Rollenanalyse, die eine massive Simulation steuert, ohne dass die Framerate auch nur um einen Frame einbricht. Ich gestehe, dass ich selbst nicht ganz glauben konnte, dass das funktioniert, bis es funktionierte.
V
Schließlich stieg ich noch einmal hinab in den Maschinenraum der GPU: die Fragment-Shader, in denen die eigentlichen Flocking-Regeln berechnet werden. Shader-Code verzeiht keine Verschwendung: Ein ineffizienter Befehl wird 16 Millionen Mal pro Frame bestraft.
Drei Stellen im Shader-Code, jede für sich unscheinbar:
Die Quake-Formel → Um die Flugrichtung eines Vogels anzupassen, muss ich Vektoren normalisieren: Länge berechnen (Quadratwurzel), Vektor durch diese Länge teilen. Divisionen und Quadratwurzeln sind aufwändig. GPUs besitzen allerdings einen spezialisierten Hardware-Befehl für den reziproken Kehrwert der Quadratwurzel. Statt durch eine Wurzel zu teilen, multiplizieren wir damit. Ein mathematischer Trick, direkt in Silizium gegossen, der die Normalisierung nahezu instant erledigt.
Loop-Invarianten eliminieren → Ich berechnete die Blickrichtung des aktuellen Vogels in jedem einzelnen Durchlauf der inneren Schleife. Bei 4096 Vögeln: 4096-mal dieselbe Berechnung pro Vogel. Ich zog sie einfach vor die Schleife. Ein klassischer Anfängerfehler, und ich war ja schließlich einer, dessen Behebung mir sofort massig GPU-Leistung zurückgab.
Organic Noise statt Value Noise → Echte Vögel zittern und flattern im Wind. Dafür nutzte ich bisher ein klassisches 3D-Rauschen (Value Noise), das Dutzende Zufalls- und Interpolationsberechnungen erforderte. Ich ersetzte es durch ein Geflecht aus ineinander verschachtelten Sinus- und Cosinuswellen unterschiedlicher Frequenzen. Es verbraucht 80 % weniger Trigonometrie-Befehle und erzeugt eine Bewegung, die sich noch natürlicher, sanfter und weniger digital anfühlt.
VI
Wenn ich heute auf die Tauri-App blicke, die mit minimaler CPU-Last und wenigen Megabytes butterweich über meinen Bildschirm gleitet, wird mir klar: Diese Optimierungs-Odyssee hat die ursprüngliche Metapher nur noch vertieft.
Vor einem Jahr lernte ich, dass aus simplen Regeln komplexe Systeme entstehen. Heute weiß ich, wie man solche Systeme am Leben erhält, wenn sie wachsen. Und ja, das reicht für mindestens einen LinkedIn-Post:
Der Ballast-Abwurf → zeigt, dass eine Organisation nicht groß sein muss, um Großes zu leisten. Manchmal müssen wir die gigantischen Infrastrukturen hinter uns lassen und auf schlanke, native Schnittstellen setzen.
Die CPU-Diät → lehrt, alltäglichen Müll zu beseitigen. Wie viele temporäre Objekte erzeugen wir in Form unnötiger Abstimmungsrunden und Daten-Duplikate, die am Ende nur den organisatorischen Garbage Collector beschäftigen und das operative Tempo drosseln?
Der asynchrone Blick → ist das schönste Plädoyer für Delegation statt Mikromanagement. Wer als Führungskraft ständig die Arbeit blockiert, um den exakten Zwischenstand abzufragen, erzeugt Stillstand. Asynchroner Informationsfluss hält die Pipeline flüssig.
Die Vögel fliegen weiter, schneller, schlanker und eleganter als zuvor. Und während sie im virtuellen Aufwind kreisen, erinnern sie daran: Wahre Eleganz zeigt sich nicht darin, wie viel wir einem System hinzufügen, sondern darin, was wir weglassen können, ohne dass es an Magie verliert.
PS: Die komplett optimierte, federleichte Desktop-App für macOS gibt es nach wie vor hier zum Download, und den asynchronen Flug kannst du selbst erleben.
Titelbild: Die Murmuration App in Aktion
Resoniert das? Schreib mir, teile den Text oder abonniere den Newsletter.