- YJIT und ZJIT sind JIT-Compiler-Architekturen in Ruby 3.x, die Ruby-Code in Maschinensprache umwandeln, um die Ausführungsgeschwindigkeit zu erhöhen
- YJIT zählt die Aufrufe jeder Funktion oder jedes Blocks und wandelt den entsprechenden Code in Maschinensprache um, sobald ein bestimmter Schwellenwert erreicht ist
- Der umgewandelte Code wird in YJIT-Blöcken gespeichert; jeder Block wird in ARM64-Maschinensprachbefehle übersetzt, die mehreren YARV-Anweisungen entsprechen
- Mit Branch Stubs werden zur Laufzeit die tatsächlichen Datentypen beobachtet und passend dazu selektiv Maschinensprachbefehle erzeugt
- Diese Struktur ist ein zentraler Mechanismus, um sowohl die Ausführungsleistung von Ruby zu steigern als auch dynamische Typisierung effizient zu verarbeiten
Kapitel 4: Ruby in Maschinensprache kompilieren
Interpreting vs. Compiling Ruby Code
- Im Originaltext sind keine Details enthalten
Counting Method and Block Calls
- YJIT verfolgt die Anzahl der Funktions- und Blockaufrufe eines Programms, um Hotspot-Code zu identifizieren
- Neben der YARV-Befehlssequenz jeder Funktion oder jedes Blocks werden jit_entry- und jit_entry_calls-Werte gespeichert
jit_entry ist anfangs null und speichert später einen Zeiger auf den von YJIT erzeugten Maschinencode
jit_entry_calls wird bei jedem Aufruf um 1 erhöht
- Sobald die Aufrufzahl den Schwellenwert erreicht, kompiliert YJIT den betreffenden Code in Maschinensprache
- Der Standard-Schwellenwert in Ruby 3.5 beträgt 30 Aufrufe für kleine Programme und 120 Aufrufe für große Anwendungen
- Zur Laufzeit kann er mit der Option
--yjit-call-threshold geändert werden
- So wandelt YJIT nur häufig ausgeführten Code in Maschinensprache um und schafft damit einen effizienten Ausführungspfad
YJIT Blocks
- YJIT speichert die erzeugten Maschinensprachbefehle in YJIT-Blöcken
- YJIT-Blöcke sind nicht dasselbe wie Ruby-Blöcke, sondern entsprechen Teilbereichen von YARV-Anweisungen
- Jede Ruby-Funktion oder jeder Ruby-Block besteht aus mehreren YJIT-Blöcken
- Im Beispielprogramm beginnt YJIT mit der Kompilierung, wenn der Block zum 30. Mal ausgeführt wird
- Die erste YARV-Anweisung
getlocal_WC_1 wird in Maschinensprache übersetzt, wodurch ein neuer YJIT-Block entsteht
- Anschließend wird die Anweisung
getlocal_WC_0 zusätzlich kompiliert und in denselben Block aufgenommen
- Laut Figure 4-8 erzeugt YJIT ARM64-Befehle, die Werte in die Register x1 und x9 des M1-Prozessors laden
getlocal_WC_1 speichert die lokale Variable des vorherigen Stack-Frames, getlocal_WC_0 die Variable des aktuellen Stacks im Stack
- Die erzeugten Maschinensprachbefehle führen dieselbe Operation aus
YJIT Branch Stubs
- Wenn YJIT die Anweisung
opt_plus kompiliert, tritt das Problem auf, dass der Operandtyp unbekannt ist
- Je nach Typ wie Ganzzahl, String oder Gleitkommazahl sind unterschiedliche Maschinensprachbefehle erforderlich
- Beispiel: Für Integer-Addition wird der Befehl
adds verwendet, für Gleitkomma-Addition ist ein anderer Befehl nötig
- Um dieses Problem zu lösen, verwendet YJIT Laufzeitbeobachtung statt Vorabanalyse
- Während der Programmausführung prüft es die Typen der tatsächlich übergebenen Werte und erzeugt darauf abgestimmten Maschinencode
- Für dieses Verhalten werden Branch Stubs eingesetzt
- Wenn ein neuer Branch noch keinen verbundenen Block hat, wird er vorübergehend mit einem Stub verknüpft
- Sobald der tatsächliche Typ bekannt ist, wird dieser Stub durch den passenden Block ersetzt
ZJIT (nur erwähnt)
- Im Inhaltsverzeichnis gibt es einen Abschnitt zu ZJIT, im Haupttext jedoch keine konkrete Erklärung
Zusammenfassung
- YJIT ist in Ruby 3.5 ein JIT-Compiler, der die Ausführungseffizienz einer dynamisch typisierten Sprache verbessern soll
- Zentrale Elemente sind ein Kompilierungs-Trigger auf Basis der Aufrufzahl, die YJIT-Blockstruktur und die Typprüfung zur Laufzeit über Branch Stubs
- Auf der ARM64-Architektur wird in echte Maschinensprachbefehle übersetzt, um die Ausführungsgeschwindigkeit von Ruby-Code zu erhöhen
- ZJIT wird als JIT der nächsten Generation erwähnt, Details dazu fehlen jedoch im Text
1 Kommentare
Hacker-News-Kommentare
Früher gab es MacRuby, das mithilfe von LLVM auf macOS zu nativem Code kompiliert wurde und sich in Objective‑C-Frameworks integrieren ließ
Das war eine ziemlich coole Idee, aber am Ende scheint Apple mit Swift eine andere Richtung eingeschlagen zu haben
Wenn eine neue Ausgabe erscheint, will ich mir das Buch Ruby Under a Microscope unbedingt kaufen und lesen. Ich mag Ruby immer noch, hatte aber nicht oft die Gelegenheit, es tatsächlich zu verwenden
Heute wird es zwar von anderen weitergeführt, aber derzeit scheint der Fokus eher auf DragonRuby zu liegen, einer auf Spiele ausgerichteten Ruby-Implementierung
Zur Referenz gibt es auch den Wikipedia-Artikel
Allerdings werden manche älteren APIs möglicherweise nicht mehr unterstützt
Mit VB6 konnte man wirklich sehr schnell entwickeln und auch Direct3D oder ASP Classic einsetzen
Die Eleganz und Entwicklerfreundlichkeit von Ruby erinnert mich an diese Zeit
Wenn Ruby GUI-Werkzeuge auf dem Niveau von VB6 gehabt hätte, wäre seine Popularität vielleicht ganz anders verlaufen
Es freut mich sehr zu sehen, dass Pat das Projekt weiterführt
Sein erstes Buch Ruby Under a Microscope und seine Blogposts haben mich stark inspiriert
Ich habe ihn früher sogar einmal direkt auf der Euruko-Konferenz getroffen, und er war wirklich ein großartiger Mensch
Als ich Ruby Under a Microscope zum ersten Mal gelesen habe, hatte ich wirklich viel Spaß damit
Dadurch konnte ich es damals sogar beim Lösen von CTF-Aufgaben nutzen
In letzter Zeit habe ich die internen Implementierungsdetails von Ruby nicht mehr verfolgt, aber wenn eine neue Ausgabe erscheint, werde ich sie auf jeden Fall kaufen
Dieser Beitrag hat mich dazu gebracht, die neue Ausgabe des Buchs wieder lesen zu wollen
Wo wir gerade über Ruby-Kompilierung sprechen: Ich frage mich, ob jemand schon einmal den Sorbet compiler ausprobiert hat, den Entwickler bei Stripe gebaut haben
Ankündigung zur Open-Source-Veröffentlichung des Sorbet Compiler
AOT-Kompilierung ist bei Ruby wirklich schwierig
Der Ansatz von Sorbet ist deshalb interessant, weil sich auf Basis der Typprüfung von Ruby schnelle Pfade erzeugen lassen
Ich arbeite selbst als privates Projekt an einem Ruby-Compiler und orientiere mich dabei an hokstad.com/compiler und
writing-a-compiler-in-ruby
Im Moment konzentriere ich mich darauf, RubySpec zu bestehen, und später möchte ich auch typbasierte Optimierungen versuchen
Nicht direkt mit Ruby-Kompilierung verbunden, aber das Buch Enterprise Integration with Ruby hat mir große Einblicke gegeben, wie man Ruby auch außerhalb des Webs einsetzen kann
Seit ich MRuby kennengelernt habe, habe ich großen Spaß daran, meine Projekte und Skripte in eigenständige ausführbare Dateien zu verwandeln
Es freut mich, dass Ruby Under a Microscope immer noch aktualisiert wird
Für alle, die verstehen wollen, wie Ruby intern funktioniert, ist es meiner Meinung nach Pflichtlektüre
Ich habe mich gefragt, wie YJIT nachverfolgt, wie ein Block für verschiedene Eingabetypen kompiliert wird, wenn er mehrfach ausgeführt wird
Ich wollte verstehen, wie Ruby mit unterschiedlichen Typen wie int oder float umgeht
Es verwendet einen „wait‑and‑see“-Ansatz, bei dem die Kompilierung aufgeschoben wird, bis tatsächliche Typen vorliegen
Für jeden Typ wird eine eigene Blockversion verwaltet und situationsabhängig aufgerufen
Dieser Algorithmus heißt Basic Block Versioning
Maxime Chevalier‑Boisvert von Shopify erklärt das sehr gut im RubyConf-2021-Vortrag
Die neue JIT-Engine ZJIT scheint einen anderen Ansatz zu verwenden
Dynamisch typisierte Sprachen per JIT schnell zu machen, erkauft man sich normalerweise mit höherem Speicherverbrauch
Für Unternehmen, die nicht so groß wie Shopify sind, könnte das ein noch größeres Problem sein
Heutige Cloud-Instanzen bieten oft etwa 4 GiB RAM pro Core, daher sind einige hundert MB JIT-Code normalerweise gut verkraftbar
Es wirkte auf mich simpel, dass YJIT nur die Anzahl der Funktionsaufrufe zählt, um Hotspots zu finden
Ich habe mich gefragt, ob es nicht auch wie bei JavaScript-JITs schwere Berechnungen innerhalb von Schleifen erkennen kann
Die Blockstruktur von Ruby könnte bei solchen Optimierungen vielleicht hilfreich sein
Dadurch kann der JIT Blöcke wie separate Funktionen behandeln und Schleifen ganz natürlich optimieren
Auf diesen Teil werde ich im nächsten Kapitel noch ausführlicher eingehen