Entmystifizierung von BEAM-Binaries: Wie wir unsere DNS-Library blitzschnell gemacht haben (ganz ohne NIFs)
Wenn man eine Plattform betreibt, die Domain-Registrierungen, DNS-Hosting und Cloud-Infrastruktur verwaltet – so wie wir bei NameOcean –, dann ist Performance nicht nur ein nettes Extra, sondern das Fundament der Zuverlässigkeit. Deshalb freuen wir uns besonders über clevere Optimierungsgeschichten aus dem Erlang-Ökosystem, und deshalb möchten wir eine unserer Favoriten für Sie aufschlüsseln.
Kürzlich sind wir auf einen faszinierenden Deep-Dive des DNSimple-Teams gestoßen, der sich mit der Optimierung von dns_erlang befasst, ihrer Open-Source-DNS-Bibliothek. Die Ergebnisse waren beeindruckend: 12 % weniger CPU-Last pro Abfrage und 14 % mehr pro Sekunde bediente Anfragen – und das alles allein durch reine BEAM-Optimierungen, ganz ohne NIFs. Für alle, die Hochdurchsatz-Systeme in Elixir oder Erlang bauen, ist dies Pflichtlektüre.
Lassen Sie uns analysieren, was das möglich macht.
Das BEAM-Binär-Mentalmodell
Etwas, das viele Elixir-Entwickler überrascht: Das Anhängen an Binärdaten in einer engen Schleife ist nicht so teuer, wie man denkt.
Wenn Sie etwas schreiben wie:
def build_response(acc, []) do
acc
end
def build_response(acc, [h | t]) do
build_response(<<acc::binary, h>>, t)
end
Würde man erwarten, dass dies O(n²) ist – wobei jede Iteration eine neue Binärdatei erstellt und alles kopiert. Aber die BEAM ist smarter.
Da Binärdaten unveränderlich (immutable) sind, kann der Compiler beweisen, dass es sicher ist, direkt in den bestehenden Speicher zu schreiben, wenn acc gerade erst durch ein vorheriges Anhängen erstellt wurde und keine andere Stelle eine Referenz darauf hält. Das erste Anhängen reserviert etwas Pufferkapazität, und nachfolgende Anhänge schreiben direkt hinein. Das Ergebnis? Amortisiertes O(1) pro Anhängen – identisch mit der Funktionsweise eines std::vector in C++.
Der Haken? Wenn Ihre Binärdaten „entweichen“ (escape) – das heißt, sie werden in einem Prozess-Dictionary gespeichert, an einen Aufrufer zurückgegeben oder an etwas anderes angehängt – ist die Party vorbei. Dann kehren Sie zum Kopieren zurück.
Dieses Muster begegnet einem überall bei der Verarbeitung von Binärprotokollen. Wenn Sie DNS-Antworten iterativ aufbauen, ist es enorm wichtig, diesen Akkumulator-Fluss kompakt zu halten.
Sub-Binärdaten: Kostenloses Slicing (fast)
Das Pattern-Matching auf Binärdaten erzeugt das, was die BEAM „Sub-Binärdaten“ nennt – winzige Terme, die auf die Originaldaten zeigen, ohne sie zu kopieren. Wenn Sie schreiben:
<<Header:12/binary, Rest/binary>> = Packet
Werden keine Daten kopiert. Rest ist lediglich ein Fenster in das Originalpaket.
Doch hier liegt die Subtilität, die viele verunsichert: Die BEAM behält intern einen Match-Kontext bei, wenn Sie rekursiv matchen – im Grunde ein Cursor, der durch die Bytes wandert. Es wird erst dann eine echte Sub-Binärdatei materialisiert, wenn Sie sie tatsächlich „entweichen“ lassen – indem Sie sie zurückgeben, speichern oder an eine Stelle übergeben, an der sie verbleibt.
Das ist für das DNS-Parsing von enormer Bedeutung. Jede Abfrage kommt als Binärdaten an, wird gegen die Protokollstruktur geprüft und muss eine Antwort erzeugen. Zu verstehen, wann man neue Arbeit erzeugt versus wann man Daten nur betrachtet, ist der Unterschied zwischen einem Parser, der skaliert, und einem, der unter Last kapituliert.
Warum die Integer-Darstellung wichtiger ist, als man denkt
Auf einer 64-Bit-BEAM benötigen Integer einige Bits für ihr Type-Tag. Das lässt 60 Bits für den eigentlichen Wert übrig. Integer, die in diesen Bereich passen, sind „Small Integers“ – unmittelbare Werte, die direkt in den Prozess-Registern leben und niemals den Heap berühren.
Alles, was größer ist, wird zu einem „Bignum“, was eine Heap-Allokation und die Berücksichtigung der Garbage Collection erfordert. Das klingt esoterisch, wird aber kritisch, wenn man die Art von Bit-Manipulation betreibt, die Binärprotokolle verlangen.
Die Technik nennt sich SWAR (SIMD Within A Register) – das Packen mehrerer Werte in einen einzigen Integer, damit man sie auf CPU-Ebene parallel verarbeiten kann. Für DNS bedeutet das, viele Bytes an Daten in jedem Vorgang zu parsen. Aber das funktioniert nur, wenn die gepackten Werte in den „Sweet Spot“ der Small Integers passen.
Wenn nicht, zahlen Sie nicht nur die Kosten für die Speicherallokation – Sie lösen potenziell Garbage-Collection-Zyklen aus, die sonst nicht existieren würden. Bei Millionen von Abfragen pro Sekunde summieren sich diese Zyklen.
Was das für Ihren Code bedeutet
Hier ist das praktische Fazit: Sie müssen keine NIFs schreiben oder zu C greifen, um ernsthafte Performance-Gewinne zu erzielen. Sie müssen verstehen, wie die BEAM über Daten denkt.
Ein paar Muster, die man verinnerlichen sollte:
1. Halten Sie Binär-Akkumulatoren kompakt. Append-only-Schleifen sind Ihr Freund. Sobald eine Binärdatei entweicht, ändert sich das Kostenmodell dramatisch.
2. Matchen Sie nach Möglichkeit „in place“. Nutzen Sie Pattern Matching, um Daten zu betrachten, anstatt sie zu kopieren. Die BEAM ist darauf optimiert.
3. Achten Sie auf Ihre Integer-Größen. Wenn Sie Arithmetik oder Bit-Manipulationen durchführen, denken Sie darüber nach, ob Ihre Werte im Bereich der „Small Integers“ bleiben.
4. Profilen Sie mit Blick auf Reductions. Die BEAM plant Arbeit in „Reductions“ (ungefähr Funktionsaufrufe). Eine heiße Schleife, die mehr Matching in weniger Funktionsaufrufen erledigt, verursacht aus Sicht des Schedulers weniger Arbeit.
Für alle, die Systeme bauen, die Binärprotokolle verarbeiten – sei es DNS, WebSocket-Frames oder maßgeschneiderte Protokollimplementierungen – gelten diese Muster direkt. Die BEAM bietet Ihnen unglaubliche Werkzeuge für schnellen Code, aber sie sind nur nützlich, wenn Sie das Modell verstehen.
Wir haben erlebt, dass Teams zu NIFs oder Rust-NIFs greifen, obwohl ein tieferes Verständnis der BEAM-Semantik ihnen 90 % der Gewinne ohne jegliche Komplexität geliefert hätte. Die Laufzeitumgebung ist smarter, als wir ihr oft zutrauen – unsere Aufgabe ist es, Code zu schreiben, der sie glänzen lässt.
Bei NameOcean denken wir ständig über Performance im großen Maßstab nach. Egal, ob Sie Vibe Hosting für Ihr nächstes Projekt starten oder das DNS für eine hochfrequentierte Anwendung verwalten: Das Verständnis dieser Grundlagen hilft Ihnen, Systeme zu bauen, die schnell bleiben, wenn es hektisch wird.
Viel Spaß beim Coden, und mögen Ihre Binärdaten immer effizient angehängt werden.