Эффективная загрузка модулей WebAssembly

При работе с WebAssembly вам часто нужно загрузить модуль, скомпилировать его, создать его экземпляр, а затем использовать то, что он экспортирует в JavaScript. В этой статье объясняется наш рекомендуемый подход для оптимальной эффективности.

При работе с WebAssembly вы часто хотите загрузить модуль, скомпилировать его, создать его экземпляр, а затем использовать то, что он экспортирует в JavaScript. Этот пост начинается с распространенного, но неоптимального фрагмента кода, который делает именно это, обсуждает несколько возможных оптимизаций и в конечном итоге показывает самый простой и эффективный способ запуска WebAssembly из JavaScript.

Этот фрагмент кода выполняет полный цикл загрузки-компиляции-создания экземпляра, хотя и неоптимальным способом:

Не используйте это!

(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);
})();

Обратите внимание, как мы используем new WebAssembly.Module(buffer) для превращения буфера ответа в модуль. Это синхронный API, то есть он блокирует основной поток до тех пор, пока не завершится. Чтобы воспрепятствовать его использованию, Chrome отключает WebAssembly.Module для буферов размером более 4 КБ. Чтобы обойти ограничение по размеру, мы можем использовать 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) все еще не является оптимальным подходом, но мы вернемся к этому через секунду.

Почти каждая операция в измененном фрагменте теперь асинхронна, как ясно показывает использование await . Единственным исключением является new WebAssembly.Instance(module) , который имеет то же ограничение на размер буфера в 4 КБ в Chrome. Для согласованности и ради сохранения основного потока свободным мы можем использовать асинхронный 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);
})();

Давайте вернемся к оптимизации compile , на которую я намекнул ранее. При потоковой компиляции браузер может уже начать компилировать модуль WebAssembly, пока байты модуля еще загружаются. Поскольку загрузка и компиляция происходят параллельно, это быстрее — особенно для больших полезных нагрузок.

Когда время загрузки больше, чем время компиляции модуля WebAssembly, то WebAssembly.compileStreaming() завершает компиляцию почти сразу после загрузки последних байтов.

Чтобы включить эту оптимизацию, используйте WebAssembly.compileStreaming вместо WebAssembly.compile . Это изменение также позволяет нам избавиться от промежуточного буфера массива, поскольку теперь мы можем напрямую передавать экземпляр Response , возвращаемый 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);
})();

API WebAssembly.compileStreaming также принимает обещание, которое разрешается в экземпляр Response . Если у вас нет необходимости в response где-либо еще в вашем коде, вы можете передать обещание, возвращаемое fetch , напрямую, без явного await его результата:

(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);
})();

Если вам не нужен результат fetch где-либо еще, вы можете даже передать его напрямую:

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

Хотя лично мне кажется более читабельным разместить это на отдельной строке.

Видите, как мы компилируем ответ в модуль, а затем немедленно создаем его экземпляр? Как оказалось, WebAssembly.instantiate может компилировать и создавать экземпляр за один раз. API WebAssembly.instantiateStreaming делает это в потоковом режиме:

(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);
})();

Если вам нужен только один экземпляр, нет смысла хранить объект module , что еще больше упрощает код:

// 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);
})();

Примененные нами оптимизации можно обобщить следующим образом:

  • Используйте асинхронные API, чтобы избежать блокировки основного потока
  • Используйте потоковые API для более быстрой компиляции и создания экземпляров модулей WebAssembly
  • Не пишите код, который вам не нужен

Удачи в работе с WebAssembly!