Dieser Leitfaden richtet sich an Webentwickler, die von WebAssembly profitieren möchten, lernen Sie, wie Sie mit Wasm CPU-intensive Aufgaben mithilfe der eines laufenden Beispiels. Der Leitfaden deckt alles ab – von Best Practices für das Laden von Wasm-Modulen, um die Kompilierung und Instanziierung zu optimieren. Es behandelt die Verlagerung der CPU-intensiven Aufgaben auf Web Worker und untersucht Implementierungsentscheidungen treffen, z. B. wann das Web erstellt werden soll, und ob er dauerhaft aktiviert oder bei Bedarf hochgefahren werden soll. Die der Leitfaden entwickelt den Ansatz iterativ und führt ein Leistungsmuster ein. bis die beste Lösung für das Problem vorgeschlagen wird.
Annahmen
Angenommen, Sie haben eine sehr CPU-intensive Aufgabe, die Sie auslagern möchten.
WebAssembly (Wasm) für seine nahezu native Leistung. Die CPU-intensive Aufgabe
die in diesem Leitfaden als Beispiel verwendet werden, um die Fakultät einer Zahl zu berechnen. Die
Fakultät ist das Produkt einer Ganzzahl und aller darunter liegenden Ganzzahlen. Für
Beispiel: Die Fakultät von vier (geschrieben als 4!
) ist gleich 24
(d. h.
4 * 3 * 2 * 1
. Die Zahlen werden schnell groß. Beispiel: 16!
ist
2,004,189,184
. Ein realistischeres Beispiel für eine CPU-intensive Aufgabe könnte sein:
Scannen eines Barcodes oder
Rasterbild verfolgen.
Eine performante iterative (im Gegensatz zu einer rekursiven) Implementierung einer factorial()
wird im folgenden in C++ geschriebenen Codebeispiel gezeigt.
#include <stdint.h>
extern "C" {
// Calculates the factorial of a non-negative integer n.
uint64_t factorial(unsigned int n) {
uint64_t result = 1;
for (unsigned int i = 2; i <= n; ++i) {
result *= i;
}
return result;
}
}
Im weiteren Verlauf des Artikels nehmen wir an, es gibt ein Wasm-Modul, das auf der Kompilierung
Diese factorial()
-Funktion mit Emscripten in einer Datei namens factorial.wasm
mit allen
Best Practices für die Code-Optimierung.
Weitere Informationen zur Vorgehensweise finden Sie unter
Kompilierte C-Funktionen mit ccall/cwrap aus JavaScript aufrufen
Mit dem folgenden Befehl wurde factorial.wasm
kompiliert als
eigenständigen Wasm verwendet.
emcc -O3 factorial.cpp -o factorial.wasm -s WASM_BIGINT -s EXPORTED_FUNCTIONS='["_factorial"]' --no-entry
In HTML gibt es einen form
mit einem input
, kombiniert mit einem output
und einem Senden
button
. Auf diese Elemente wird in JavaScript über ihren Namen verwiesen.
<form>
<label>The factorial of <input type="text" value="12" /></label> is
<output>479001600</output>.
<button type="submit">Calculate</button>
</form>
const input = document.querySelector('input');
const output = document.querySelector('output');
const button = document.querySelector('button');
Laden, Kompilieren und Instanziieren des Moduls
Bevor Sie ein Wasm-Modul verwenden können, müssen Sie es laden. Im Web passiert das
über die
fetch()
der API erstellen. Wie Sie wissen, benötigt Ihre Webanwendung das Wasm-Modul
CPU-intensive Aufgabe sollten Sie die Wasm-Datei so früh wie möglich vorab laden. Ich
tun Sie dies mit einem
CORS-fähiger Abruf
im Bereich <head>
Ihrer App.
<link rel="preload" as="fetch" href="factorial.wasm" crossorigin />
In Wirklichkeit ist die fetch()
API asynchron und Sie müssen das await
Ergebnis.
fetch('factorial.wasm');
Kompilieren und instanziieren Sie als Nächstes das Wasm-Modul. Es gibt verlockende Namen
aufgerufene Funktionen
WebAssembly.compile()
(plus
WebAssembly.compileStreaming()
)
und
WebAssembly.instantiate()
für diese Aufgaben, sondern das
WebAssembly.instantiateStreaming()
.
kompiliert und ein Wasm-Modul direkt aus einem gestreamten
zugrunde liegende Quelle wie fetch()
– kein await
erforderlich. Dies ist die effizienteste
und eine optimierte Methode zum Laden von Wasm-Code. Angenommen, das Wasm-Modul exportiert
factorial()
, können Sie sie sofort verwenden.
const importObject = {};
const resultObject = await WebAssembly.instantiateStreaming(
fetch('factorial.wasm'),
importObject,
);
const factorial = resultObject.instance.exports.factorial;
button.addEventListener('click', (e) => {
e.preventDefault();
output.textContent = factorial(parseInt(input.value, 10));
});
Aufgabe an einen Web Worker übertragen
Wenn Sie dies im Hauptthread mit wirklich CPU-intensiven Aufgaben ausführen, die gesamte App blockieren. Eine gängige Praxis ist die Verlagerung solcher Aufgaben Worker.
Umstrukturierung des Hauptthreads
Um die CPU-intensive Aufgabe in einen Web Worker zu verlagern, besteht der erste Schritt in der Neustrukturierung
der Anwendung. Der Hauptthread erstellt jetzt einen Worker
. Darüber hinaus
geht es nur um das Senden der Eingabe an den Web Worker und den anschließenden Empfang der
und Anzeige dieser Ausgabe.
/* Main thread. */
let worker = null;
// When the button is clicked, submit the input value
// to the Web Worker.
button.addEventListener('click', (e) => {
e.preventDefault();
// Create the Web Worker lazily on-demand.
if (!worker) {
worker = new Worker('worker.js');
// Listen for incoming messages and display the result.
worker.addEventListener('message', (e) => {
output.textContent = e.result;
});
}
worker.postMessage({ integer: parseInt(input.value, 10) });
});
Schlecht: Die Aufgabe wird in Web Worker ausgeführt, aber der Code ist nicht jugendfrei
Der Web Worker instanziiert das Wasm-Modul und nach dem Empfang einer Nachricht
die CPU-intensive Aufgabe ausführt und das Ergebnis zurück an den Hauptthread sendet.
Das Problem bei diesem Ansatz ist,
dass die Instanziierung eines Wasm-Moduls
WebAssembly.instantiateStreaming()
ist ein asynchroner Vorgang. Das bedeutet,
dass der Code nicht jugendfrei ist. Im schlimmsten Fall sendet der Hauptthread Daten,
Web Worker ist noch nicht bereit und der Web Worker empfängt die Nachricht nicht.
/* Worker thread. */
// Instantiate the Wasm module.
// 🚫 This code is racy! If a message comes in while
// the promise is still being awaited, it's lost.
const importObject = {};
const resultObject = await WebAssembly.instantiateStreaming(
fetch('factorial.wasm'),
importObject,
);
const factorial = resultObject.instance.exports.factorial;
// Listen for incoming messages, run the task,
// and post the result.
self.addEventListener('message', (e) => {
const { integer } = e.data;
self.postMessage({ result: factorial(integer) });
});
Besser: Die Aufgabe wird in Web Worker ausgeführt, allerdings mit möglicherweise redundantem Laden und Kompilieren.
Eine Problemumgehung für das Problem der asynchronen Instanziierung des Wasm-Moduls besteht darin, Laden, Kompilieren und Instanziieren des Wasm-Moduls in das Ereignis verschieben aber das würde bedeuten, dass diese Arbeit empfangene Nachricht. Mit HTTP-Caching und dem HTTP-Cache können kompiliertem Wasm-Bytecode entspricht, ist dies nicht die schlechteste Lösung, aber es gibt eine bessere Lösung,
Durch Verschieben des asynchronen Codes an den Anfang des Web Workers sondern darauf zu warten, dass das Versprechen erfüllt wird, sondern es in einem wird sofort mit dem Ereignis-Listener-Teil der und es gehen keine Nachrichten aus dem Hauptthread verloren. Einblicke in die Veranstaltung kann das Versprechen abwarten.
/* Worker thread. */
const importObject = {};
// Instantiate the Wasm module.
// 🚫 If the `Worker` is spun up frequently, the loading
// compiling, and instantiating work will happen every time.
const wasmPromise = WebAssembly.instantiateStreaming(
fetch('factorial.wasm'),
importObject,
);
// Listen for incoming messages
self.addEventListener('message', async (e) => {
const { integer } = e.data;
const resultObject = await wasmPromise;
const factorial = resultObject.instance.exports.factorial;
const result = factorial(integer);
self.postMessage({ result });
});
Gut: Die Aufgabe wird in Web Worker ausgeführt und wird nur einmal geladen und kompiliert.
Das Ergebnis der statischen
WebAssembly.compileStreaming()
ist ein Versprechen, das zu einer
WebAssembly.Module
Ein tolles Merkmal dieses Objekts ist, dass es mit
postMessage()
Das bedeutet, dass das Wasm-Modul nur einmal im Hauptmodul geladen und kompiliert werden kann.
oder einem anderen Web Worker, der sich nur mit dem Laden und Kompilieren befasst,
und dann an den Web Worker übergeben, der für die CPU-intensive
. Der folgende Code zeigt diesen Ablauf.
/* Main thread. */
const modulePromise = WebAssembly.compileStreaming(fetch('factorial.wasm'));
let worker = null;
// When the button is clicked, submit the input value
// and the Wasm module to the Web Worker.
button.addEventListener('click', async (e) => {
e.preventDefault();
// Create the Web Worker lazily on-demand.
if (!worker) {
worker = new Worker('worker.js');
// Listen for incoming messages and display the result.
worker.addEventListener('message', (e) => {
output.textContent = e.result;
});
}
worker.postMessage({
integer: parseInt(input.value, 10),
module: await modulePromise,
});
});
Auf der Web Worker-Seite müssen nur noch die WebAssembly.Module
extrahiert werden.
-Objekt zu erstellen und zu instanziieren. Da die Nachricht mit der WebAssembly.Module
nicht
verwendet, verwendet der Code im Web Worker
WebAssembly.instantiate()
statt der instantiateStreaming()
-Variante wie zuvor. Die instanziierte
Modul wird in einer Variablen zwischengespeichert, sodass die Instanziierung nur ausgeführt werden muss.
beim Hochfahren des Web Workers ein.
/* Worker thread. */
let instance = null;
// Listen for incoming messages
self.addEventListener('message', async (e) => {
// Extract the `WebAssembly.Module` from the message.
const { integer, module } = e.data;
const importObject = {};
// Instantiate the Wasm module that came via `postMessage()`.
instance = instance || (await WebAssembly.instantiate(module, importObject));
const factorial = instance.exports.factorial;
const result = factorial(integer);
self.postMessage({ result });
});
Perfekt: Die Aufgabe wird in Inline-Web Worker ausgeführt und nur einmal geladen und kompiliert.
Auch bei HTTP-Caching ist es sinnvoll, den (idealerweise) im Cache gespeicherten Web Worker-Code abzurufen und
ist potenziell teuer. Ein häufiger Trick besteht darin,
Fügen Sie den Web Worker inline hinzu und laden Sie ihn als blob:
-URL. Dazu ist weiterhin die
kompiliertes Wasm-Modul, das zur Instanziierung an den Web Worker übergeben wird,
Kontext des Web Workers und des Hauptthreads unterscheiden, auch wenn sie
die auf derselben JavaScript-Quelldatei basieren.
/* Main thread. */
const modulePromise = WebAssembly.compileStreaming(fetch('factorial.wasm'));
let worker = null;
const blobURL = URL.createObjectURL(
new Blob(
[
`
let instance = null;
self.addEventListener('message', async (e) => {
// Extract the \`WebAssembly.Module\` from the message.
const {integer, module} = e.data;
const importObject = {};
// Instantiate the Wasm module that came via \`postMessage()\`.
instance = instance || await WebAssembly.instantiate(module, importObject);
const factorial = instance.exports.factorial;
const result = factorial(integer);
self.postMessage({result});
});
`,
],
{ type: 'text/javascript' },
),
);
button.addEventListener('click', async (e) => {
e.preventDefault();
// Create the Web Worker lazily on-demand.
if (!worker) {
worker = new Worker(blobURL);
// Listen for incoming messages and display the result.
worker.addEventListener('message', (e) => {
output.textContent = e.result;
});
}
worker.postMessage({
integer: parseInt(input.value, 10),
module: await modulePromise,
});
});
Verzögerte oder eifere Web Worker-Erstellung
Bisher haben alle Codebeispiele den Web Worker bei Bedarf langsam hochgefahren, wenn die Taste gedrückt wurde. Je nach Anwendung kann es sinnvoll sein, um den Web Worker zu erstellen, z. B. wenn die App inaktiv ist des Bootstrapping-Prozesses der App. Verschieben Sie daher die Web Worker-Erstellung außerhalb des Ereignis-Listeners der Schaltfläche befindet.
const worker = new Worker(blobURL);
// Listen for incoming messages and display the result.
worker.addEventListener('message', (e) => {
output.textContent = e.result;
});
Web Worker in der Nähe halten oder nicht
Möglicherweise sollten Sie sich fragen, ob Sie den Web Worker dauerhaft verfügbar zu machen oder bei Bedarf neu zu erstellen. Beide Ansätze sind und haben ihre Vor- und Nachteile. Wenn Sie z. B. ein Web die dauerhaft in der Umgebung arbeiten, gleichzeitige Aufgaben erschweren, da Sie irgendwie Ergebnisse zuordnen müssen. die vom Web Worker auf die Anfragen zurückkommen. Die Web- und Der Bootstrapping-Code der Worker ist möglicherweise ziemlich komplex, sodass unter Umständen wenn Sie jedes Mal ein neues erstellen. Zum Glück können Sie dies mit dem User Timing API.
Die Codebeispiele enthalten bisher einen Web Worker. Die folgenden im Codebeispiel wird bei Bedarf ein neuer Web Worker Ad-hoc erstellt. Sie benötigen den Überblick über Beenden des Web Workers selbst. Das Code-Snippet überspringt die Fehlerbehandlung, falsch ist, sollten Sie in jedem Fall beenden, ob bei Erfolg oder Misserfolg.)
/* Main thread. */
let worker = null;
const modulePromise = WebAssembly.compileStreaming(fetch('factorial.wasm'));
const blobURL = URL.createObjectURL(
new Blob(
[
`
// Caching the instance means you can switch between
// throw-away and permanent Web Worker freely.
let instance = null;
self.addEventListener('message', async (e) => {
// Extract the \`WebAssembly.Module\` from the message.
const {integer, module} = e.data;
const importObject = {};
// Instantiate the Wasm module that came via \`postMessage()\`.
instance = instance || await WebAssembly.instantiate(module, importObject);
const factorial = instance.exports.factorial;
const result = factorial(integer);
self.postMessage({result});
});
`,
],
{ type: 'text/javascript' },
),
);
button.addEventListener('click', async (e) => {
e.preventDefault();
// Terminate a potentially running Web Worker.
if (worker) {
worker.terminate();
}
// Create the Web Worker lazily on-demand.
worker = new Worker(blobURL);
worker.addEventListener('message', (e) => {
worker.terminate();
worker = null;
output.textContent = e.data.result;
});
worker.postMessage({
integer: parseInt(input.value, 10),
module: await modulePromise,
});
});
Demos
Es gibt zwei Demos, mit denen Sie experimentieren können. Eine mit einem
Ad-hoc-Web Worker
(Quellcode)
und eines mit einer
Dauerhafter Web Worker
(Quellcode).
Wenn Sie die Chrome-Entwicklertools öffnen und die Console aufrufen, sehen Sie
Timing API-Protokolle, die die Zeit messen, die vom Klicken auf die Schaltfläche bis zum
auf dem Bildschirm angezeigt wird. Auf dem Tab "Network" wird die URL "blob:
" angezeigt.
Anfrage(n). In diesem Beispiel ist der zeitliche Unterschied
zwischen Ad-hoc- und Dauerauftrag
ungefähr das 3-Fache. In der Praxis sind sie für das menschliche Auge
Fall. Die Ergebnisse für Ihre eigene echte App werden wahrscheinlich anders ausfallen.
Ergebnisse
In diesem Beitrag wurden einige Leistungsmuster beim Umgang mit Wasm untersucht.
- In der Regel sollten Sie Streamingmethoden
(
WebAssembly.compileStreaming()
undWebAssembly.instantiateStreaming()
) im Vergleich zu den entsprechenden Dateien ohne Streaming (WebAssembly.compile()
undWebAssembly.instantiate()
. - Wenn möglich, lagern Sie leistungsintensive Aufgaben in einem Web Worker aus und führen Sie den Wasm durch.
nur einmal außerhalb des Web Workers laden und kompilieren. Auf diese Weise
Der Web Worker muss nur das Wasm-Modul instanziieren, das er vom Hauptmodul erhält
in dem das Laden und Kompilieren mit
WebAssembly.instantiate()
bedeutet, dass die Instanz im Cache gespeichert werden kann, wenn Sie den Web Worker dauerhaft verfügbar zu halten. - Prüfen Sie sorgfältig, ob es sinnvoll ist, einen dauerhaften Web Worker zu behalten. oder Ad-hoc-Web Worker erstellen, wenn sie gebraucht werden. Ebenfalls Überlegen Sie, wann der beste Zeitpunkt für die Erstellung des Web Workers ist. Wichtige Hinweise sind der Arbeitsspeicherverbrauch, die Instanziierungsdauer von Web Workern, sondern auch die Komplexität der Verarbeitung gleichzeitiger Anfragen.
Wenn Sie diese Muster berücksichtigen, sind Sie auf dem richtigen Weg, Wasm-Leistung.
Danksagungen
Dieser Leitfaden wurde geprüft von Andreas Haas Jakob Kummerow, Deepti Gandluri, Alon Zakai Francis McCabe, François Beaufort und Rachel Andrew.