JIT-Kompilierung
PetaPerl enthält einen JIT-Compiler (Just-In-Time), der auf Cranelift basiert — demselben Backend, das auch von Wasmtime verwendet wird. Der JIT kompiliert häufig ausgeführte Schleifen in nativen Maschinencode und liefert dabei Leistung, die perl5 um Größenordnungen übertreffen kann.
Was JIT-kompiliert wird
Der JIT zielt auf Schleifen ab — for und while — die Arithmetik- und Vergleichsoperationen enthalten. Im Einzelnen:
Ganzzahlige For-Schleifen
my $sum = 0;
for my $i (1..1_000_000) {
$sum += $i;
}
Der JIT erkennt das Ganzzahl-Akkumulator-Muster und kompiliert den Schleifenrumpf in nativen Code. Die Schleifenvariable ($i) und Akkumulatoren ($sum) werden in CPU-Registern gehalten.
While-Schleifen mit Gleitkomma-Arithmetik
while ($y < $max_y) {
my $cx = $x * $scale;
my $cy = $y * $scale;
# ... Berechnung ...
$y += $step;
}
Gleitkomma-Arithmetik (+, -, *, /), Vergleiche (<, >, <=, >=) und Kontrollfluss (if/last) innerhalb von While-Schleifen werden in native SSE/AVX-Instruktionen kompiliert.
Verschachtelte Schleifen
Der JIT verarbeitet verschachtelte Schleifen bis zu 5 Ebenen tief. Jede Verschachtelungsebene erzeugt ihren eigenen Satz von Cranelift-Basisblöcken mit korrekten Phi-Knoten-Verbindungen für Variablen, die zwischen den Ebenen fließen.
String-Operationen (über Extern Calls)
Der JIT unterstützt .= (Concat-Assign) und $x = "" (Leeren) auf String-Variablen durch externe Funktionsaufrufe aus JIT-kompiliertem Code zurück in die Rust-Laufzeitumgebung.
Was nicht JIT-kompiliert wird
- Unterprogrammaufrufe — der Overhead von Funktionsaufrufen dominiert, der JIT-Vorteil ist marginal
- Regex-Operationen — die Regex-Engine hat ihren eigenen Optimierungspfad
- I/O-Operationen — I/O-gebundener Code profitiert nicht vom JIT
- Komplexer Datenstrukturzugriff — Hash-/Array-Operationen mit dynamischen Schlüsseln
- String-intensive Berechnungen — String-Aufbau wird stattdessen durch die PvBuf-Optimierung behandelt
Code, der nicht JIT-kompiliert wird, läuft weiterhin auf dem Interpreter, der seine eigenen Schnellpfade für häufige Operationen besitzt.
Leistung
Ackermann-Funktion
Der Interpreter-Schnellpfad (nicht JIT) verarbeitet rekursive Ganzzahlarithmetik:
| Laufzeitumgebung | Zeit | Beschleunigung |
|---|---|---|
| perl5 | 630ms | 1,0x |
| pperl (Interpreter) | 14ms | 45x schneller |
Mandelbrot-Menge (1000x1000)
JIT-Kompilierung verschachtelter While-Schleifen mit Gleitkomma-Arithmetik:
| Laufzeitumgebung | Zeit | Beschleunigung |
|---|---|---|
| perl5 | 12.514ms | 1,0x |
| pperl (nur JIT) | 163ms | 76x schneller |
| pperl (JIT + parallel) | 29ms | 431x schneller |
Wie das erreicht wird
- Registerallokation: Schleifenvariablen in CPU-Registern statt auf dem Interpreter-Stack
- Typspezialisierung: Variablen, die nachweislich Ganzzahl oder Gleitkomma sind, verwenden direkt native Instruktionen
- Branch-Eliminierung: Konstante Bedingungen werden zur Kompilierzeit entfernt
- Kein Dispatch-Overhead: Nativer Code ersetzt die Interpreter-Dispatch-Schleife innerhalb JIT-kompilierter Bereiche vollständig
CLI-Steuerung
# Standard: JIT aktiviert
pperl script.pl
# JIT deaktivieren (nur Interpreter)
pperl --no-jit script.pl
Der Testrahmen läuft standardmäßig mit --no-jit, um die Korrektheit des Interpreters zu testen. JIT-spezifische Tests in t/62-jit/ überschreiben dies.
Architektur
Kompilierungspipeline
Schleife erkannt → Variablen und Typen analysieren → JIT-IR aufbauen
→ Über Cranelift kompilieren → Kompilierte Funktion cachen → Nativen Code ausführen
Caching
Kompilierte Funktionen werden anhand der enterloop-Op-ID in der Op-Arena gecacht. Ein CachedWhileLoop speichert:
- Den kompilierten nativen Funktionszeiger
- Variablen-Typinformationen (Gleitkomma vs. Ganzzahl vs. String)
- Einen konstanten String-Pool (für String-Operationen)
- Metadaten zur Eignung für parallele Ausführung
Nachfolgende Iterationen derselben Schleife verwenden die gecachte Kompilierung wieder.
JIT-IR
Der JIT verwendet eine eigene Zwischendarstellung (JitIr), die Perl-Operationen auf Cranelift-Operationen abbildet:
JitIr::WhileLoop { condition_ir, body_ir }— rekursiv für VerschachtelungJitIr::FloatVar/JitIr::IntVar— typisierter VariablenzugriffJitIr::BinOp— Arithmetik und VergleicheJitIr::ExitIfFalse/JitIr::ExitIfTrue— Schleifenaustritt undlast
Variablentypen
Der JIT verfolgt zwei Variablentypen:
JitType::F64— Gleitkommawerte in einem f64-PufferJitType::Ptr— String-Werte, die über Extern Calls an die Rust-Laufzeitumgebung zugegriffen werden
Variablen werden anhand ihres Verwendungsmusters im Schleifenrumpf typisiert. Variablen mit gemischten Typen fallen auf den Interpreter zurück.