2 Punkte von GN⁺ 2024-07-31 | 1 Kommentare | Auf WhatsApp teilen
  • Porffor ist ein Forschungsprojekt, das JavaScript nicht zur Laufzeit ausführt, sondern vorab zu WebAssembly und nativen Binärdateien kompiliert
  • Da kein Interpreter mitgebündelt wird, zielt es darauf ab, Ausgaben zu erzeugen, die 10- bis 30-mal kleiner und schneller sind als bei bestehenden JS→Wasm-Projekten
  • Auch bei nativen Builds wird keine Runtime paketiert, wodurch die Binärgröße um bis zu den Faktor 1000 schrumpfen kann; ein Beispiel sinkt von etwa 90 MB auf unter 100 KB
  • Porffor ist in JS geschrieben, kommt ohne eval aus und stellt native Unterstützung für TypeScript ohne separaten Build-Schritt heraus
  • AOT ist vorteilhaft für Optimierungen durch statische Analyse und Kompilierung vor der Ausführung, erschwert aber dynamische JS-Auswertung wie eval und befindet sich noch in einer frühen Phase, in der vieles JS noch nicht funktioniert

Die von Porffor geschaffene Ausführungsweise

  • Porffor ist ein Forschungsprojekt, das JavaScript Ahead-of-Time zu WebAssembly und nativen Binärdateien kompiliert
  • Eine mit Porffor kompilierte TypeScript-Binärdatei stellt diese Seite bereit
  • Es wurde von Anfang an mit AOT im Blick geschrieben und ist so aufgebaut, dass es Optimierungen versucht, die bei bisherigen JS-Ausführungsweisen schwierig waren

Unterschiede zwischen WebAssembly- und nativer Ausgabe

  • JS → Wasm

    • Die WebAssembly-Ausgabe von Porffor ist 10- bis 30-mal kleiner und schneller als bei bestehenden JS→Wasm-Projekten
    • Der zentrale Unterschied liegt darin, dass JS direkt kompiliert wird und kein Interpreter gebündelt wird
    • Wenn JS in Wasm ausgeführt wird, ist Sandbox-Ausführung möglich, kann aber große Performance-Einbußen verursachen; Porffor konzentriert sich darauf, diese Kosten zu reduzieren
    • Mögliche Einsatzfälle:
      • Serverseitiges JS-Hosting: In Edge-Runtimes kann Wasm-Sandboxing sichere Ausführung ohne übermäßige Isolation bieten
      • Der geringe Overhead von AOT schafft gegenüber JIT die Möglichkeit, auf derselben Hardware mehr Kunden mit minimalen Performance-Einbußen auszuführen
      • Widerstand gegen Reverse Engineering: Bei sensiblem JS kann kompilierter Code schwieriger zurückzuentwickeln sein als Obfuskation
  • JS → Native

    • Da JS tatsächlich kompiliert und keine Runtime paketiert wird, kann die Größe von Binärdateien um bis zu den Faktor 1000 kleiner werden
    • Die Beispielgröße beträgt etwa 90 MB → unter 100 KB
    • Intern wird JS zunächst nach C kompiliert und anschließend nativ kompiliert; daher kann JS dort eingesetzt werden, wo C genutzt werden kann
    • Mögliche Einsatzfälle:
      • Schnelle JS-Ausführung in Embedded-Systemen, Spielkonsolen und ähnlichen Umgebungen
      • Kleine JS-CLI-Apps, die zu One-Click-Executables unter 1 MB kompiliert werden

Vorteile und Einschränkungen von AOT

  • Traditionelle Interpreter oder mehrere JIT-Stufen müssen zwischen Startzeit und JS-Performance abwägen
  • AOT kompiliert zuerst und führt später aus; daher ist die Kompiliergeschwindigkeit zwar wichtig für die Developer Experience, beeinflusst aber nicht die User Experience
  • Dieser Ansatz eröffnet Spielraum für Optimierungen auf Basis statischer Analyse, wie bei C++ und Rust
  • Der wichtigste Nachteil ist, dass dynamische JS-Auswertung wie eval fehlt und eine JS-Engine neu gebaut werden muss
  • Da sich Porffor noch in einer frühen Phase befindet, funktioniert vieles JS noch nicht, aber die Verbesserungsarbeit läuft
  • Um den Fortschritt bei der ECMAScript-Kompatibilität zu verfolgen, wird bei jedem Commit die offizielle Testsuite Test262 ausgeführt

1 Kommentare

 
GN⁺ 2024-07-31
Meinungen auf Hacker News
  • Oliver, der Hauptentwickler von Porffor, hat angekündigt, Vollzeit an Porffor zu arbeiten: https://x.com/canadahonk/status/1818347311417938237

  • Ich habe über etwas Ähnliches nachgedacht, halte es aber für schwierig, mit JavaScript deutlich bessere Performance zu erzielen. Wahrscheinlich wäre das Beste, JS in V8-C++-Aufrufe zu transpilen
    Wirklich spannende Optimierungen entstehen, wenn man TypeScript oder etwas Ähnliches kompiliert. Durch Typen lässt sich viel herausholen, und untypisierte Teile würden im Grunde auf langsame JS-Aufrufe zurückfallen. Interfaces könnte man auf virtuelle Funktionstabellen oder direkte Aufrufe reduzieren, und man könnte auf Strukturen statt auf Maps arbeiten. Man könnte Int- und Float-Typen haben, die bei Bedarf zu Number herabgestuft werden, und sie in Registern halten
    Das Kernproblem ist, dass sowohl TS als auch V8 sich schnell verändernde, nicht standardisierte Ziele sind. Für so ein Projekt braucht man ein großes Team, und allein die Kompatibilität zu erhalten wird zu einer eigenen Aufgabe

    • Ohne zusätzliche Erweiterungen hilft TypeScript weniger, als man denkt. Es wurde ursprünglich nicht für diesen Zweck entworfen
      Ein einfaches Beispiel: TypeScript unterscheidet nicht zwischen Ganzzahlen und Gleitkommazahlen, sondern behandelt alles als Zahl. Deshalb braucht jeder Array-Zugriff eine Typumwandlung. Wäre TypeScript zur Unterstützung statischer Kompilierung entworfen worden, gäbe es diese Unterscheidung vermutlich
      Das größere Problem ist TypeScripts strukturelle Subtypisierung. Wegen dieser Eigenschaft ist es für den Compiler praktisch unmöglich, die physische Struktur nicht-primitiver Argumente, die an eine Funktion übergeben werden, statisch zu bestimmen. Ein JIT kann dynamische Shape-Analysen durchführen, daher kann man bei jedem Feldzugriff schlechtere Performance als mit einem JIT bekommen
    • Als Porffor-Contributor stimme ich nicht zu. Auch bei JavaScript gibt es zur Kompilierzeit ziemlich viel Verbesserungsspielraum
      Es gab viel Arbeit an statischen Typanalyse-Tools für JS, und auch sehr gründliche Analysen sind möglich. Ein Beispiel, das mir einfällt, ist das etwas ältere TAJS
    • Ein Projekt, das in gewisser Weise mit dieser Idee verwandt ist, ist AssemblyScript: https://www.assemblyscript.org
    • ECMAScript 4 war ein Versuch, bessere Typen in die Sprache zu bringen, ist aber leider vor langer Zeit gescheitert
      Es wäre schön, wenn man zumindest in TypeScript Typen wie integer angeben könnte. Auch wenn eine normale TS→JS-Kompilierung const val: int genauso behandeln würde wie const val: number, könnten moderne Runtimes, die TS verstehen, diese Zusatzinformation nutzen
      Ich frage mich, ob eine Syntax wie const counter: Number akzeptiert werden könnte
    • Du sagst „Ich habe darüber nachgedacht, aber es ist schwierig, bessere Performance zu bekommen“, und beschreibst dann einen Ansatz, der genau das verlangt, was direkt oben auf der Startseite der Website erklärt wird
      Ich weiß nicht, ob sich die Website geändert hat oder ob ich etwas übersehen habe
  • Bei windmill.dev nutzen wir Bun build, wenn Nutzer Code deployen, um Skripte und alle Abhängigkeiten in eine einzelne JS-Datei zu bündeln; diese laden wir dann, um Cold Starts und Speicherverbrauch zu verbessern. Wegen der Bundle-Größe speichern wir das Ergebnis in S3
    Wenn man alles nativ bündeln könnte, wäre das ein völlig anderes Spiel. So gut die Cold Starts von Bun auch sind: Gegen die direkte native Ausführung eines kleinen Binaries wird es schwer zu gewinnen sein

    • Als Entwickler stimme ich zu. Das wirkt wie ein interessanter Anwendungsfall, bei dem Porffor potenziell helfen könnte. Es wäre schön, irgendwann darüber zu sprechen
  • Es ist schön zu sehen, wie mehr JS-Runtimes den Zugang zu Wasm angehen. Dieses Projekt erinnert mich an Static Hermes, Facebooks JS-Engine zur Beschleunigung von iOS und Android in React-Native-Projekten.
    Beide zielen auf Konformität mit JS test262 ab; Porffor unterstützt sowohl native als auch Wasm-Ausgabe, während Static Hermes derzeit vor allem auf native Ausgabe fokussiert ist. Porffor ist in reinem JS geschrieben und bewegt sich in Richtung Selbstkompilierung, Static Hermes hängt von LLVM ab. Porffor hatte bislang nur eingeschränkte Unterstützung für async/promise/await, Static Hermes unterstützt sie mit einigen Einschränkungen. Static Hermes ist in C++ geschrieben, Porffor hauptsächlich in JS. Beide unterstützen TypeScript, aber Static Hermes transpiliert den TS-AST nach Flow, während Porffor es nativ unterstützt. Static Hermes hat für schwer zu kompilierende JS-Situationen wie eval einen Fallback-Interpreter, Porffor unterstützt nur Ahead-of-Time-Kompilierung.
    Insgesamt bin ich gespannt, ob dieses Projekt Fahrt aufnehmen und JavaScript-Engines am Edge schneller machen kann. Hinterlassen als Syrus von Wasmer.
    https://github.com/facebook/hermes/discussions/1137
    https://github.com/tc39/test262
    https://wasmer.io

    • Zur Einordnung: Static Hermes unterstützt das Kompilieren von JS nach WASM vollständig. Da es bereits ein LLVM-Backend gibt, bekommt man diese Funktion praktisch geschenkt. Ein Beispiel gibt es unter https://x.com/tmikov/status/1706138872412074204
      Das ist allerdings nicht unser Fokus; wir konzentrieren uns hauptsächlich auf React Native. In dieser Umgebung bringt WASM nicht viel.
      Die wichtigste Funktion von Static Hermes ist der Type Checker, der Laufzeit-Soundness garantiert. Porffor ist sehr interessant, ich beobachte es schon eine Weile und drücke die Daumen, dass es gut läuft.
    • Als Porffor-Contributor finde ich den Vergleich gut. Allerdings unterstützt Porffor technisch gesehen ebenfalls Promises. Sie laufen nur synchron.
      Ein ähnlicher Ansatz wie bei Kiesel: https://kiesel.dev/
    • Ein paar kleine Korrekturen: Porffor ist noch nicht vollständig self-hosted, aber wir gehen davon aus, dass es möglich sein wird. Einige Built-ins wie Array.prototype.filter, Math.sin und atob kompilieren sich jedoch teilweise selbst.
      In letzter Zeit hat Porffor außerdem begonnen, grundlegendes async/promise/await zu unterstützen. Es funktioniert nur noch nicht besonders gut.
    • Das klang so, als wäre die Abhängigkeit von LLVM etwas Schlechtes.
  • JavaScript hat eine Teilmenge, die sich leicht kompilieren lässt, und das Schwierige ist der lange Rest außerhalb davon. Trotzdem ist es großartig, dass untersucht wird, wo diese Grenze liegt und wie viel Gewinn man aus dieser Teilmenge ziehen kann.

  • Ich finde es wirklich gut, dass String.blink unterstützt wird. Dass Entwickler Humor und Verspieltheit haben, ist immer ein gutes Zeichen.

    • Wenn man will, dass sich ein „ECMAScript-Host wie ein Webbrowser verhält“, muss man es natürlich unterstützen. Es ist Teil der Spezifikation: https://tc39.es/ecma262/multipage/additional-ecmascript-feat...
      Auch die Implementierung ist trivial, etwa auf dem Niveau von function() { return "" + this + ""; }, daher kann es sich selbst für einen ECMAScript-Host lohnen, der kein Webbrowser ist. In diesem Fall ist es optional. Ich würde nicht erwarten, dass das etwas mit „Humor oder Verspieltheit“ zu tun hat.
    • String.blink ist in test262 enthalten, daher muss es faktisch unterstützt werden, wenn das Projekt sein Ziel erreichen will.
  • Ich frage mich, welche Nuance ich übersehen habe. Mir ist nicht klar, warum „Ahead-of-Time-JS-Engine“ eine bessere Beschreibung sein soll als „JS-to-Wasm-Compiler“. Wenn es hauptsächlich eine Framing-Strategie ist, ist das auch in Ordnung.

    • Es gibt bereits Projekte, die einen JS-Interpreter bündeln und damit JS-to-WASM machen. Daher ist die Formulierung wahrscheinlich dazu gedacht, den Unterschied zu diesem Ansatz deutlicher zu machen.
  • Das hier beschriebene Versionsschema wirkt auf mich etwas fragwürdig.
    Wenn eine Änderung Regressionen in einigen Test262-Tests verursacht, könnte damit auch die Versionsnummer zurückgehen. Das heißt, Porffor kann nicht zugleich monoton steigende Versionsnummern haben und die Fähigkeit, dass notwendige Änderungen Test262-Regressionen verursachen.
    https://github.com/CanadaHonk/porffor?tab=readme-ov-file#ver...

    • Vermutlich ist die Absicht, Arbeiten, die Test262-Regressionen verursachen, nur temporär und auf einem separaten Branch durchzuführen und sie erst dann in main zu mergen, wenn auch alle nötigen Fixes zur Beseitigung der Regressionen enthalten sind. Eine neue Versionsnummer müsste dann erst nach diesem Merge verwendet werden.
  • Bedeutet auf Walisisch „violett“.

    • Die Etymologie geht auf das griechische Wort für violett zurück; das wohl häufigste englische Wort mit derselben Wurzel ist porphyry, ein violettes Mineral.
  • Es ist erfrischend zu sehen, dass mehrere JS-Engines für unterschiedliche Zwecke entstehen.
    Um Plugins in Anwendungen einzubetten, habe ich daran gearbeitet, quickjs über llrt mehr Node-kompatible APIs bereitzustellen.
    https://github.com/awslabs/llrt