このガイドは、WebAssembly を利用したいと考えているウェブ デベロッパー向けに、 Wasm を利用して CPU 使用率の高いタスクをアウトソーシングする方法、 実行例のヘルプをご覧ください。ベスト プラクティスから、Google Workspace の Wasm モジュールの読み込みによるコンパイルとインスタンス化の最適化。これは、 さらに、CPU 使用率の高いタスクをウェブ ワーカーにシフトすることについて説明し、 実装に関する意思決定が頭に浮かびます ワーカーを永続的に存続させるか、必要に応じてスピンアップするかを指定します。「 手法を繰り返し開発し、1 つのパフォーマンス パターンを導入 問題の最適な解決策を提案するまで、一度に一つずつ進めていきます。
前提条件
CPU 使用率が非常に高く、そのタスクをアウトソーシングする必要があると仮定します。
ネイティブに近いパフォーマンスを提供する WebAssembly(Wasm)CPU 使用率の高いタスク
を使用して、数値の階乗を計算します。「
階乗は、ある整数とその下のすべての整数の積です。対象
たとえば、4 の階乗(4!
と記述)は 24
と等しくなります(つまり、
4 * 3 * 2 * 1
)。数字は急速に大きくなります。たとえば、16!
は次のようになります。
2,004,189,184
。CPU 使用率の高いタスクのより現実的な例は、
バーコードのスキャンや
ラスター画像のトレースを行います。
factorial()
の(再帰ではなく)高パフォーマンスの反復実装
関数を、C++ で記述された次のコードサンプルに示しています。
#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;
}
}
この記事の残りの部分では、
factorial.wasm
というファイルに Emscripten を含む factorial()
関数がある
すべて使用
コード最適化のベスト プラクティスをご覧ください。
方法については、
ccall/cwrap を使用して JavaScript からコンパイルされた C 関数を呼び出す。
factorial.wasm
をコンパイルするには、次のコマンドを使用します。
スタンドアロン Wasm。
emcc -O3 factorial.cpp -o factorial.wasm -s WASM_BIGINT -s EXPORTED_FUNCTIONS='["_factorial"]' --no-entry
HTML には、input
と output
を組み合わせた form
があり、submit があります。
button
。これらの要素は、名前に基づいて JavaScript から参照されます。
<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');
モジュールの読み込み、コンパイル、インスタンス化
Wasm モジュールを使用する前に、モジュールを読み込む必要があります。ウェブ上では
から
fetch()
APIご存じのとおり、ウェブアプリはインフラストラクチャの Wasm モジュールに依存
CPU 使用率の高いタスクの場合は、できるだけ早く Wasm ファイルをプリロードする必要があります。マイページ
使用します。
CORS 対応の取得
(アプリの <head>
セクション内)。
<link rel="preload" as="fetch" href="factorial.wasm" crossorigin />
実際には、fetch()
API は非同期であるため、await
表示されます。
fetch('factorial.wasm');
次に、Wasm モジュールをコンパイルしてインスタンス化します。Google Meet の
呼び出される関数
WebAssembly.compile()
(プラス
WebAssembly.compileStreaming()
)
および
WebAssembly.instantiate()
担いますが、代わりに
WebAssembly.instantiateStreaming()
メソッドはコンパイルし、ストリーミングされたインスタンスから直接 Wasm モジュールをインスタンス化
fetch()
などの基盤となるソースがあり、await
は必要ありません。この方法が最も効率的です。
Wasm コードを読み込むための最適な方法ですWasm モジュールが BigQuery の
factorial()
関数を呼び出すと、すぐに使用できます。
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));
});
タスクをウェブワーカーにシフトする
CPU 使用率が非常に高いタスクでメインスレッドで実行すると、 アプリ全体をブロックします。一般的には、そのようなタスクをウェブ アプリケーションに移行し、 ワーカー。
メインスレッドの再構成
CPU 使用率の高いタスクをウェブ ワーカーに移動するには、まず、
確認します。メインスレッドは Worker
を作成するようになりました。それとは別に、
入力をウェブ ワーカーに送信してから、メッセージを受け取るだけです。
表示されます。
/* 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) });
});
悪い例: タスクはウェブ ワーカーで実行されるが、コードは際どい
ウェブワーカーは Wasm モジュールをインスタンス化し、メッセージを受信すると
CPU 使用率の高いタスクを実行し、結果をメインスレッドに返します。
この方法の問題は、
WebAssembly.instantiateStreaming()
は非同期オペレーションです。つまり
際立たなくなります。最悪の場合、メインスレッドが
ウェブ ワーカーの準備ができておらず、ウェブ ワーカーはメッセージを受信しません。
/* 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) });
});
良い例: タスクはウェブワーカーで実行されるが、読み込みとコンパイルが冗長になる可能性がある
非同期 Wasm モジュールのインスタンス化に関する問題の回避策の一つは、 Wasm モジュールの読み込み、コンパイル、インスタンス化のすべてをイベントに移動する つまり、この作業はすべてのインスタンスに対して 受信メッセージを返します。HTTP キャッシュと HTTP キャッシュで これは最悪のソリューションではありませんが、 できます。
非同期コードを Web Worker の先頭に移動して、 実際には Promise が解決されるのを待機しますが、Promise を 変数を渡すと、プログラムはすぐにイベント リスナー部分に メインスレッドからのメッセージが失われることはありません。イベントの内容 Promise が待機されます。
/* 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 });
});
良い例: タスクはウェブ ワーカーで実行され、読み込みとコンパイルが 1 回だけ行われる
静的な
WebAssembly.compileStreaming()
メソッドは Promise で解決され、
WebAssembly.Module
。
このオブジェクトの優れた機能の一つは、
postMessage()
。
つまり、Wasm モジュールの読み込みとコンパイルはメイン モジュールで一度だけ
(読み込みとコンパイルだけを扱う別のウェブ ワーカーでも)
CPU 使用率の高いトラフィックを処理するウェブ ワーカーに
タスクを実行します。次のコードに、このフローを示します。
/* 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,
});
});
ウェブワーカー側では、あとは WebAssembly.Module
を抽出するだけです。
インスタンス化してインスタンス化しますWebAssembly.Module
を含むメッセージは
ウェブ ワーカーのコードでは、
WebAssembly.instantiate()
以前の instantiateStreaming()
バージョンではなく、インスタンス化された
変数にキャッシュされるため、インスタンス化作業は
Web Worker の起動時に 1 回表示されます。
/* 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 });
});
完璧: タスクはインライン ウェブ ワーカーで実行され、一度だけ読み込まれてコンパイルされる
HTTP キャッシュを使用する場合でも、キャッシュされたウェブ ワーカーのコードを取得し、
コストが高くなる可能性がありますパフォーマンスのコツとして、
ウェブワーカーをインライン化し、blob:
URL として読み込みます。この場合も、
インスタンス化のためにウェブ ワーカーに渡す、コンパイル済みの Wasm モジュールは、
メインスレッドのコンテキストは、たとえメインスレッドが
基づいています。
/* 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,
});
});
ウェブ ワーカーの作成の遅延または先行する
ここまでのところ、すべてのコードサンプルでウェブ ワーカーはオンデマンドで遅延していました。つまり、 トリガーします。アプリケーションによっては、アプリケーション コードを より積極的にウェブワーカーを作成するようにします。たとえば、アプリがアイドル状態のときや、 自動的に行われます。そのため、ウェブワーカーで作成されたジョブを ボタンのイベント リスナーの外側に置く必要があります。
const worker = new Worker(blobURL);
// Listen for incoming messages and display the result.
worker.addEventListener('message', (e) => {
output.textContent = e.result;
});
Web Worker を常に使用する
1 つの疑問点は、ウェブ ワーカーを永続ボリュームに保持すべきかどうかを 永続的に再利用することも、必要なときにいつでも再作成することもできます。どちらの方法も メリットとデメリットがあります。たとえば、ウェブ アプリケーションを ワーカーを永続的に動作させると、アプリのメモリ使用量が増加し、 なんらかの方法で結果をマッピングする必要があるため、同時実行タスクの処理が難しくなる リクエストに戻ります一方 ワーカーのブートストラップ コードはかなり複雑になる可能性があるため、 オーバーヘッドが発生するためです。幸いこれは 測定します User Timing API:
ここまでのコードサンプルでは、1 つの永続的なウェブ ワーカーを保持しています。次の いつでも新しいウェブ ワーカーをアドホックに作成できます。なお、 追跡するために ウェブワーカーの終了 できます。(コード スニペットではエラー処理がスキップされますが、 成功か失敗かにかかわらず、すべてのケースで必ず終了してください)。
/* 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,
});
});
デモ
2 つのデモを試すことができます。1 つは
アドホック ウェブワーカー
(ソースコード)
もう 1 つは
永続的なウェブワーカー
(ソースコード)。
Chrome DevTools を開いてコンソールを確認すると、
ボタンのクリックから
表示されます。[ネットワーク] タブに blob:
の URL が表示される
できます。この例では、アドホックと永続化のタイミングの
約 3 倍です。実際には、この 2 つは人間には見分けがつかないので、
あります。実際のアプリでは、結果はそれぞれ異なります。
まとめ
この投稿では、Wasm を扱うためのパフォーマンス パターンをいくつか見てきました。
- 原則として、ストリーミング方式を優先して
(
WebAssembly.compileStreaming()
、WebAssembly.instantiateStreaming()
) 他のアセット(WebAssembly.compile()
およびWebAssembly.instantiate()
など)。 - 可能であれば、パフォーマンスが重視されるタスクをウェブワーカーでアウトソーシングして、Wasm を実行します。
読み込みとコンパイルは、ウェブワーカーの外部で 1 回だけ行います。これにより、
ウェブ ワーカーは、メインシステムから受け取った Wasm モジュールをインスタンス化するだけでよい
読み込みとコンパイルが実行された
WebAssembly.instantiate()
。次の場合にインスタンスをキャッシュに保存できます。 永続性を確保する必要があります。 - 永続ウェブ ワーカーを 1 つ保持することが適切かどうかを慎重に測定する 必要なときにいつでもアドホックのウェブワーカーを作成できます。また、 Web Worker を作成する最適なタイミングを見極めます。重要ポイント メモリ消費、ウェブワーカーのインスタンス化時間 同時リクエストを処理する必要がある場合の複雑さも増します。
これらのパターンを考慮に入れれば、 Wasm のパフォーマンス。
謝辞
このガイドをレビューしたユーザー Andreas Haas 氏 Jakob Kummerow 氏、 Deepti Gandluri Alon Zakai 氏 Francis McCabe、 François Beaufort Rachel Andrew。