Эффективная загрузка модулей 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!