Efektywne wczytywanie modułów WebAssembly

W przypadku pracy z WebAssembly często konieczne jest pobranie modułu, skompilowanie go i utworzenie jego instancji, a potem wykorzystanie wyeksportowanych danych w języku JavaScript. W tym poście opisujemy zalecane przez nas metody pozwalające uzyskać optymalną skuteczność.

Pracując w WebAssembly, często chcesz pobrać, skompilować i utworzyć wystąpienie modułu a potem użyć tego, co wyeksportuje w JavaScript. Ten post rozpoczyna się od typowego, ale nieoptymalnego kodu robi to dokładnie, omawia kilka możliwych optymalizacji, a na koniec pokazuje najprostszy i najwydajniejszy sposób uruchamiania WebAssembly z poziomu JavaScriptu.

Ten fragment kodu przedstawia pełny taniec „download-build-instantiate”, ale nie jest to optymalny sposób:

Nie używaj tego!

(async () => {
  const response = await fetch('fibonacci.wasm');
  const buffer = await response.arrayBuffer();
  const module = new WebAssembly.Module(buffer);
  const instance = new WebAssembly.Instance(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

Zwróć uwagę, jak używamy new WebAssembly.Module(buffer) do przekształcania bufora odpowiedzi w moduł. To jest synchroniczny interfejs API, co oznacza, że blokuje wątek główny do momentu zakończenia procesu. Aby zniechęcić do jego używania, Chrome wyłącza funkcję WebAssembly.Module w przypadku buforów większych niż 4 KB. Aby obejść ten problem, możemy użyj w zamian: await WebAssembly.compile(buffer):

(async () => {
  const response = await fetch('fibonacci.wasm');
  const buffer = await response.arrayBuffer();
  const module = await WebAssembly.compile(buffer);
  const instance = new WebAssembly.Instance(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

await WebAssembly.compile(buffer) to nadal nie jest optymalnym rozwiązaniem, ale zrobimy to za jakiś czas. sekunda.

Prawie każda operacja w zmodyfikowanym fragmencie jest teraz asynchroniczna, ponieważ await jasne. Jedynym wyjątkiem jest plik new WebAssembly.Instance(module), który ma taki sam bufor o wielkości 4 KB. z ograniczeniem rozmiaru w Chrome. Dla spójności i zachowania głównego wątku bezpłatne, możemy użyć asynchronicznego WebAssembly.instantiate(module)

(async () => {
  const response = await fetch('fibonacci.wasm');
  const buffer = await response.arrayBuffer();
  const module = await WebAssembly.compile(buffer);
  const instance = await WebAssembly.instantiate(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

Wróćmy do wspomnianej wcześniej optymalizacji compile. Ze strumieniowaniem kompilację danych, przeglądarka może już zacznij kompilować moduł WebAssembly, gdy jego bajty są jeszcze pobierane. Od pobrania i kompilacja odbywa się równolegle, co jest szybsze – zwłaszcza w przypadku dużych ładunków.

Gdy czas pobierania wynosi
dłuższy niż czas kompilacji modułu WebAssembly, a następnie WebAssembly.buildStreaming()
kończy kompilację niemal natychmiast po pobraniu ostatnich bajtów.

Aby włączyć tę optymalizację, użyj parametru WebAssembly.compileStreaming zamiast WebAssembly.compile. Ta zmiana pozwala nam też pozbyć się bufora tablicy pośredniej, ponieważ możemy teraz przekazywać Wystąpienie Response zostało zwrócone bezpośrednio przez usługę await fetch(url).

(async () => {
  const response = await fetch('fibonacci.wasm');
  const module = await WebAssembly.compileStreaming(response);
  const instance = await WebAssembly.instantiate(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

Interfejs API WebAssembly.compileStreaming akceptuje również obietnicę, która odnosi się do Response instancji. Jeśli nie potrzebujesz kodu response w innym miejscu w kodzie, możesz zlecić obietnicę zwracany bezpośrednio przez funkcję fetch, bez jawnego awaitingu swojego wyniku:

(async () => {
  const fetchPromise = fetch('fibonacci.wasm');
  const module = await WebAssembly.compileStreaming(fetchPromise);
  const instance = await WebAssembly.instantiate(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

Jeśli też nie potrzebujesz wyniku fetch w innym miejscu, możesz go nawet przekazać bezpośrednio:

(async () => {
  const module = await WebAssembly.compileStreaming(
    fetch('fibonacci.wasm'));
  const instance = await WebAssembly.instantiate(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

Osobiście uważam jednak, że lepiej jest umieścić go w osobnym wierszu.

Zobaczyć, jak kompilujemy odpowiedź w moduł, a następnie natychmiast tworzymy jej instancję? Jak się okazuje, WebAssembly.instantiate może jednocześnie kompilować i tworzyć instancje. Interfejs API WebAssembly.instantiateStreaming robi to w sposób strumieniowy:

(async () => {
  const fetchPromise = fetch('fibonacci.wasm');
  const { module, instance } = await WebAssembly.instantiateStreaming(fetchPromise);
  // To create a new instance later:
  const otherInstance = await WebAssembly.instantiate(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

Jeśli potrzebujesz tylko jednej instancji, nie ma sensu pozostawiania obiektu module w pobliżu, dalsze uproszczenie kodu:

// This is our recommended way of loading WebAssembly.
(async () => {
  const fetchPromise = fetch('fibonacci.wasm');
  const { instance } = await WebAssembly.instantiateStreaming(fetchPromise);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

Oto podsumowanie wprowadzonych przez nas optymalizacji:

  • Używaj asynchronicznych interfejsów API, aby uniknąć blokowania wątku głównego
  • Użycie interfejsów API strumieniowania do szybszego kompilowania i tworzenia instancji modułów WebAssembly
  • Nie pisz kodu, którego nie potrzebujesz

Baw się z WebAssembly.