Wie WebAssembly JavaScript schnell ausführen kann
(bytecodealliance.org)Intro
-
Wenn JS im Browser läuft, ist die Ausführung schnell, weil die JS-Engine des Browsers gut optimiert ist. Inzwischen wird JS aber auch in vielen anderen Umgebungen verwendet. (Serverless, Gaming-Konsolen, iOS usw.)
-
WASM ist eine Technologie, die es ermöglicht, JS in solchen Laufzeitumgebungen schnell auszuführen.
Funktionsweise
-
Wenn eine JS-Engine vorhanden ist, wird JS-Code über einen Interpreter und einen JIT-Compiler in Bytecode umgewandelt.
-
In Umgebungen ohne JS-Engine muss man die JS-Engine zusammen mit dem Code ausliefern. Indem man die JS-Engine als WASM-Modul verteilt, kann man sie portabel für verschiedene Umgebungen machen.
-
Der JS-Code läuft innerhalb einer in der WASM-Engine isolierten JS-Engine.
-
Die von der WASM-Engine verwendete JS-Engine ist SpiderMonkey, das auch Firefox nutzt.
-
WASM kann nicht selbst Maschinencode erzeugen und muss daher über JS kompiliert werden.
-
Da sich JIT aber nicht nutzen lässt, wäre es eigentlich normal, dass WASM langsam ist. Wie genau macht WASM die Ausführung von JS dann überhaupt „schneller“?
Wo wird WASM eingesetzt?
JS auf iOS verwenden (oder in Umgebungen ohne JIT)
- Gaming-Konsolen, unprivileged iOS apps, Smart-TVs usw. können aus Sicherheitsgründen kein JIT verwenden.
(→ Es wird so dargestellt, als seien Sicherheitsprobleme bei JIT-Compiling selbstverständlich, aber selbst nach Recherche ist mir der Grund nicht ganz klar.)
- Daher muss man dort einen Interpreter verwenden. Tatsächlich laufen Apps auf solchen Plattformen aber meist sehr lange und haben viel Code, sodass es sinnvoll ist, die Verlangsamung durch einen Interpreter zu vermeiden.
- Wie kann man also JS nutzen, ohne die Performance-Einbußen des Interpreters in Kauf zu nehmen?
JS in Serverless verwenden
- In Serverless-Umgebungen gibt es zwar JIT, aber das Problem sind lange Cold-Start-Zeiten, die zu höherer Latenz führen. (Allein das Laden der Engine dauert mindestens 5 ms.)
- Es gibt Optimierungstechniken, um Cold-Start-Zeiten zu verstecken, aber je besser die Netzwerkebene wird (z. B. QUIC), desto weniger bringen sie. Außerdem verlieren solche Techniken stark an Nutzen, wenn mehrere Serverless-Funktionen gleichzeitig ausgeführt werden.
- Man kann Cold-Start-Zeiten auch durch Wiederverwendung von Instanzen vermeiden, aber das bedeutet, dass sich Status zwischen Anfragen teilen, was ein Sicherheitsrisiko darstellt.
- Aus diesen Gründen packt man in der Praxis oft viel zu viel Inhalt in eine einzelne Serverless-Funktion, statt Best Practices zu befolgen.
- Wenn also allein das Cold-Start-Problem gelöst wird, braucht man viele dieser Ausweichtechniken nicht mehr, und zahlreiche Probleme verschwinden.
- WASM kapselt und isoliert JS, und der Code von WASM selbst ist kurz und einfach, dadurch leichter zu überwachen und mit geringerem Sicherheitsrisiko verbunden.
Wofür verwendet eine JS-Engine besonders viel Zeit?
Initialisierungsphase
- (Engine-Initialisierung) Das betrifft Serverless. Die Engine muss sich selbst vorbereiten und Built-in-Funktionen zur Umgebung hinzufügen. Das ist einer der Gründe, warum Cold Starts in Serverless langsam sind.
- (Anwendungsinitialisierung) Funktionen in Bytecode parsen, Speicher für Variablen reservieren, Werte an Variablen zuweisen
Laufzeitphase
- Der Durchsatz ab diesem Punkt hängt von verschiedenen Bedingungen ab.
- which language features are used
- whether the code behaves predictably from the JS engine’s point of view
- what sort of data structures are used
- whether the code runs long enough to benefit from the JS engine’s optimizing compiler
Eine JS-Engine schneller zu machen bedeutet, sowohl die Initialisierungs- als auch die Laufzeitphase zu beschleunigen. Genauer gesagt: die Zeit für die Initialisierung zu verkürzen und in der Laufzeit den Durchsatz, also die Verarbeitungsgeschwindigkeit des Codes, zu erhöhen.
Initialisierungszeit verkürzen
-
WASM verwendet einen Pre-Initializer namens Wizer, um die Initialisierungszeit zu verkürzen. (Bei kleinen Apps ist JS on WASM im Vergleich zu JS isolate ungefähr 13-mal schneller.)
-
Vor der Auslieferung des Codes führt der Pre-Initializer im Build-Schritt den gesamten JS-Code einmal bis zur Initialisierungsphase aus.
-
Dadurch liegen die JS-Codes im linearen Speicher der JS-Engine bereits als Bytecode vor, und auch die Speicherallokation ist abgeschlossen.
-
Das wird unverändert kopiert und an die Datensektion von WASM angehängt.
-
-
Wenn die JS-Engine instanziiert wird, kann sie auf alle Daten in der Datensektion zugreifen. Wenn bestimmter Speicher benötigt wird, kann er aus der Datensektion kopiert werden. Deshalb ist keine Startzeit nötig, und deshalb nennt man das Pre-Initialization.
-
Aktuell hängt die Datensektion am selben Modul wie die JS-Engine, aber künftig ist geplant, mithilfe von module linking die Datensektion als separates Modul bereitzustellen, sodass mehrere Anwendungen dieselbe JS-Engine gemeinsam nutzen können.
-
Tatsächlich ist diese Technik der Pre-Initialisierung nicht auf JS-Engines beschränkt, sondern ein Konzept, das sich auch auf Python, Ruby, Lua und andere Laufzeiten anwenden lässt.
Durchsatz erhöhen
-
Wenn JS-Code nur kurz läuft, durchläuft er ohnehin kein JIT, daher ist der Durchsatz von WASM dann ähnlich wie im Browser. Bei lange laufendem Code ist der Unterschied im Durchsatz durch den Einsatz oder Nicht-Einsatz von JIT jedoch groß.
-
Da WASM kein JIT verwenden kann, setzt es stattdessen auf AOT(ahead-of-time)-Kompilierung und übernimmt dabei Techniken, die sich aus JIT nutzen lassen.
-
Eine Optimierungstechnik von JIT ist Inline Caching: bereits früher ausgeführte Codefragmente werden beibehalten und wiederverwendet.
-
In WASM wurden häufig verwendete Muster aus JS als Stubs vorbereitet. Zum Beispiel der Zugriff auf Objekt-Properties.
-
Um eigentlich korrekt auf Objekt-Properties zuzugreifen, braucht man Informationen über Shape und Offset, die sich mit AOT nicht kennen lassen.
-
Man kann jedoch im Voraus einen Stub bauen, der mit Shape und Offset als Parametern auf eine Property zugreift. Dieser Stub-Code lässt sich an vielen Stellen wiederverwenden.
-
-
WASM legt all diese common patterns als Stubs an. Das ist unabhängig davon, wie der JS-Code konkret aussieht. Dadurch wird die Menge des Maschinencodes reduziert, den die JS-Engine erzeugen muss, die Initialisierungszeit sinkt, und auch die Cache-Lokalität verbessert sich.
-
Es wurde bestätigt, dass bereits 2kb solcher Stubs genügen, um rund 95 % des tatsächlichen JS-Codes abzudecken.
-
Solche Techniken optimieren ahead-of-time, also ohne den Codeinhalt zu kennen (ohne Profiling). Mit zusätzlichem Profiling gäbe es daher vermutlich noch weiteres Optimierungspotenzial ähnlich wie bei JIT.
- Allerdings ist schon das Profiling selbst nicht einfach, daher wird daran noch gearbeitet.
2 Kommentare
Im Zusammenhang mit den Sicherheitsproblemen von JIT wurde dies bereits einmal in einem hier früher vorgestellten Blogbeitrag des MS-Edge-Teams erwähnt. Da JIT-Engines grundsätzlich komplex sind, vergrößern sie nicht nur die Angriffsfläche, sondern Methoden wie die spekulative Optimierung (Speculative Optimization), die im JIT zur Leistungssteigerung eingesetzt werden, scheinen auch dazu zu neigen, bestimmte Muster von Sicherheitsproblemen immer wieder hervorzurufen. Deshalb soll der Anteil der JIT-bezogenen Sicherheitslücken unter den Sicherheitsfehlern von Webbrowsern recht hoch sein.
https://de.news.hada.io/topic?id=4771
https://microsoftedge.github.io/edgevr/posts/Super-Duper-Secure-Mode/
https://docs.google.com/spreadsheets/d/…
Oh, danke! Ich hatte GeekNews selbst gar nicht nachgeschaut.