Zum Inhalt springen

Benchmarks

Phasis ist bei dispatchgebundenen JavaScript etwa zwei Größenordnungen langsamer als V8. Diese Grenze ergibt sich grundsätzlich daraus, dass der Interpreter auf PHP läuft, während V8 ein nativer JIT-Compiler ist. Für Nutzer, die den Interpreter einbetten, lautet die Frage nicht „Wie schneidet er im Vergleich zu V8 ab?“, sondern „Ist er für meinen Einsatzzweck schnell genug?“ Bei den meisten Einbettungsszenarien lautet die Antwort: ja.

Wann die Grenze keine Rolle spielt

  • Templating — SSR-Rendering einer typischen Seite in deutlich unter 10 ms.
  • Validierungsregeln — vom Nutzer bereitgestelltes JS zur Formularvalidierung wird in Mikrosekunden ausgeführt.
  • Datentransformationen — array.map(transform) über Hunderte von Elementen sind in einigen zehn Millisekunden abgeschlossen.
  • Sandbox-Ausführung — kurze, vom Nutzer bereitgestellte Skripte mit einem Host-Limit von 1 Sekunde haben reichlich Spielraum.

Wann die Grenze eine Rolle spielt

  • Rechenintensive innere Schleifen — Bildverarbeitung, Reduktionen großer Arrays, Simulationen. Verlege die Berechnung in eine native PHP-Bibliothek und stelle sie als Host-Funktion bereit.
  • Animationen in Echtzeit — alles, was 60 fps antreibt. Dieses JS gehört in den Client (Browser, native App), nicht in die eingebettete Engine.
  • JS-Neuimplementierungen rechenintensiver Algorithmen — Kryptografie, Komprimierung, Parsing. PHP bietet native Entsprechungen, die 1000× schneller sind; verwende diese.

Das passende Denkmodell: Phasis ist eine Klebstoffschicht für JS-artige Logik in PHP-artigen Anwendungen. Der PHP-Host übernimmt rechenintensive Aufgaben; die JS-Schicht kümmert sich um Konfiguration, Verzweigungen und kleine Transformationen.

Was die Engine (im Vergleich zu einem naiven Tree-Walker) schnell macht

  • Bytecode-VM — die meisten Aufrufe in häufig genutztem Code laufen über die Opcode-Verzweigung von VM::execute statt über den AST.
  • Benutzerdefinierter Aufrufstack — jeder JS-Aufruf hält seinen Zustand in VM::execute und legt den Frame des Aufrufers in einem Pool ab; pro JS-Aufruf wird kein neuer PHP-Frame erstellt. Siehe Bytecode-VM § Benutzerdefinierter Aufrufstack.
  • Inline-Caches — ein monomorpher Cache pro PC lässt Zugriffe auf Prototypmethoden von Instanzen (das c.method()-Muster) die Prototypenkette überspringen.
  • Frame-Snapshot-Generatoren — einfache Generator-Funktionskörper pausieren, indem sie den Ausführungszustand in einem Heap-Objekt speichern, statt eine PHP-Fiber-Grenze zu überqueren.
  • JsToPhp-Transpiler — Funktionskörper mit vielen Zahlen oder lokalen Variablen (einschließlich obj.method()-, new Ctor()- und this.member-Zugriffen) werden zu PHP-Closures kompiliert, die der Tracing-JIT von PHP 8.5 in nativen Maschinencode umwandelt.
  • JSON.stringify-Cache — JSON.stringify(o) speichert seine Ausgabe im Eingabeobjekt und invalidiert sie bei Änderungen transitiv; dadurch wird das Deep-Clone-Idiom JSON.parse(JSON.stringify(o)) beschleunigt.

Eigene Workloads messen

Shell
php bench/run.php

Damit führst du die Microbenchmark-Suite aus, die unter bench/microbench.js enthalten ist — einige synthetische Workloads (Schleifenarithmetik, rekursive Funktionen, Objekterstellung, Aufrufe von Prototypmethoden, Array-Push, JSON-Rundlauf usw.) — und gibst die mediane Laufzeit pro Test aus. Nützlich als Plausibilitätsprüfung; aktuelle Zahlen werden bei jedem CI-Bench-Lauf in BENCH.md gespeichert.

Für deinen eigenen Code kannst du ein beliebiges Skript einsetzen:

Shell
php -d xdebug.mode=off bench/run.php path/to/your-script.js

Für Profiling auf PHP-Ebene funktioniert der Profiler von Xdebug mit Phasis genauso wie mit jeder anderen PHP-Anwendung. Führe ein Skript unter xdebug.mode=profile aus und untersuche die Cachegrind-Ausgabe, um zu sehen, welche Phasis\-Klassen den größten Anteil ausmachen.

Fahrplan

Was den Abstand zu V8 verringern würde, grob nach Umsetzbarkeit geordnet:

  • Polymorphe Inline-Caches — damit eine einzelne LOAD_MEMBER-Aufrufstelle 2–4 gecachte Formen speichern kann, bevor sie auf langsame Verarbeitung zurückfällt. Hilft bei Code, der unterschiedliche Empfänger verarbeitet.
  • yield* und try-with-yield im Frame-Snapshot-Generatorpfad — diese Formen laufen derzeit über den Fiber-gestützten Tree-Walker. Bevor sie Snapshot-fähig sind, braucht jede eine explizite Zustandsmaschinenbehandlung (Weiterleitung delegierter Iteratoren, Handler-Stack bleibt beim Pausieren erhalten).
  • Asynchrone Generatoren und await auf Snapshots — laut Spezifikation wird die Generatorsuspension mit der Microtask-Warteschlange kombiniert. Sobald der Bytecode-Funktionskörper sowohl yield als auch await unterstützt, eignen sich auch asynchrone Generatoren für Snapshots.
  • Typrückschluss für Parameter in JsToPhp — Funktionsparameter sind standardmäßig numerisch typisiert, daher bricht function (obj) { obj.method() } die Kompilierung ab. Objekttypisierte Parameter anhand ihrer Verwendung im Funktionskörper zu erkennen, würde Methodenaufrufe auf Funktionsargumenten ermöglichen, ohne den Empfänger lokal über new erstellen zu müssen.
  • Direkte Ausgabe von Opcache — den eval()-Schritt von JsToPhp überspringen und Opcodes direkt im Opcache-Format schreiben. Der aufwendigste Punkt; die anderen lassen sich in der bestehenden Architektur umsetzen.

Nichts davon ist für die Einbettungsszenarien nötig, auf die Phasis ausgelegt ist.