Das Laden eines großen WebAssembly-Moduls war früher seriell: Der Browser lud die gesamte Datei in einen ArrayBuffer, übergab sie an die JavaScript-Engine und begann erst dann mit dem Kompilieren. Bei schnellen Verbindungen war das Netzwerk nicht mehr der Engpass; die CPU-Zeit für das Dekodieren und Kompilieren war es, und all das geschah nach dem Download, während der Hauptthread pausierte.
Die naheliegenden Lösungen kosteten jeweils etwas. Ein einzelner optimierender Compiler erzeugte schnellen Code, brauchte aber lange zum Start. JavaScript-Engines kompilierten zur Laufzeit lazy, was Arbeit vermied, aber die Kompilierung im Hauptthread beließ und auf das Ende des Parsens wartete. Und es schien keine Möglichkeit zu geben, vor der Ankunft der Datei zu kompilieren, weil die alte API nur einen vollständigen ArrayBuffer akzeptierte.
Firefox 58 griff beide Enden an. Die Streaming-Kompilierung leitete jeden heruntergeladenen Block über die Streaming-API direkt in die Engine, und weil der Code-Abschnitt eines wasm-Moduls vor seinem Daten-Abschnitt kommt, konnte die Kompilierung beginnen, während noch Megabyte an Daten unterwegs waren. Ein neuer gestufter Compiler fügte eine Baseline-Stufe hinzu, die Code 10- bis 15-mal schneller erzeugte als der optimierende Compiler, und ihre Ausgabe lief nur etwa 2-mal langsamer, sodass Code fast sofort ausgeführt wurde; Hintergrundthreads kompilierten dann das gesamte Modul mit dem Optimierer neu und tauschten es im laufenden Betrieb aus. Beide Compiler verteilten die Arbeit nach Funktionen auf mehrere Kerne.
Da der Kompilierdurchsatz nun den Netzwerkdurchsatz übertraf, war der Code fast in dem Moment bereit, in dem das letzte Paket ankam. Mozilla beschrieb die Änderung so, dass WebAssembly nun lädt wie das Dekodieren eines Bildes statt wie das Parsen von JavaScript: keine Kompilierpausen im Hauptthread und kein Warten auf die gesamte Nutzlast.
Bytes werden beim Eintreffen in eine schnelle Baseline-Stufe gestreamt, dieser Code wird sofort ausgeführt und im Hintergrund optimierter Code im laufenden Betrieb ausgetauscht
Das Kompilieren des ersten Pakets versteckt die Downloadzeit in CPU-Arbeit, statt Download und anschließendes Kompilieren zu serialisieren.
Eine Baseline-Stufe, die 10- bis 15-mal schneller kompiliert, macht den Start zu einem schnellen Start; der etwa 2-mal langsamere Code läuft, während die Optimierung voranschreitet.
Weil wasm-Funktionen unabhängig voneinander kompiliert werden, verteilt sich die Arbeit über mehrere Kerne, statt sich in einem Thread zu stauen.
Wenn der Code-Abschnitt eines Moduls vor seinem Daten-Abschnitt liegt, kann das Streaming den teuren Teil kompilieren, während die Nutzlast-Bytes noch eintreffen.
Jede Änderung verkürzt den Start, ohne den Maschinencode zu verändern, der schließlich läuft, sodass die Spitzenleistung unberührt bleibt.
Wenn ein Engpass-Schritt auf die gesamte Eingabe wartet, beginne mit dem ersten Block und liefere zuerst eine günstige Version; aktualisiere sie im Hintergrund, statt zu blockieren
Firefox 58 lieferte den Streaming- und den gestuften Compiler im Januar 2018 aus; die Erklärungsseite zum Start erzeugte einen Hacker-News-Thread mit 912 Punkten. Mozillas Testseite ermöglichte es Nutzern, die Kompiliergeschwindigkeit auf ihren eigenen Geräten zu messen, und das Team erklärte, die Grundlage sei gelegt, um kompilierten Maschinencode im HTTP-Cache zu speichern und damit den JavaScript-Bytecode-Cache zu erweitern, der in derselben Version ausgeliefert wurde.
DEN BELEGEN FOLGEN
Die Quellen
- Making WebAssembly even faster: Firefox's new streaming and tiering compiler Mozilla Hacks · hacks.mozilla.org ·
- Hacker News discussion: Making WebAssembly even faster Hacker News · news.ycombinator.com ·