wasm-optのAsyncifyについて

概要

Asyncify.cpp は、WebAssembly モジュールに「一時停止・再開」機能を付与するパス(変換処理)を実装している。 これにより、C/C++ などの同期的なコードを、JS などの非同期イベント駆動環境でも「途中で止めて、後から再開」できるようにする。

1. 目的

  • WebAssembly の関数呼び出しやローカル変数の状態を保存・復元し、途中で「一時停止」や「再開」ができるようにする。
  • 例: ファイル読み込みやネットワーク待ちなど、非同期イベントで一時停止し、完了後に元の関数の続きから再開できる。

2. 主な仕組み・流れ

2.1. Asyncifyの全体像

Asyncifyは、WebAssemblyバイナリを変換し、コールスタックやローカル変数の状態を保存・復元できるようにするパスである。これにより、もともと同期的なコード(C/C++など)を、非同期イベント駆動環境(JSなど)でも「一時停止・再開」できるようにする。

AsyncifyはBinaryenのパスとして実装されており、wasm-opt --asyncify などで利用できる。変換後のWasmには、スタックの巻き戻し(unwind)・再構築(rewind)を制御するためのAPIが自動的に追加される。

2.2. 状態管理とAPI

  • __asyncify_state というグローバル変数で「今の状態(通常/アンワインド中/リワインド中)」を管理する。
    • 0: 通常実行
    • 1: アンワインド(unwinding, スタックを巻き戻して一時停止)
    • 2: リワインド(rewinding, スタックを再構築して再開)
  • __asyncify_data というグローバル変数で「保存用データ構造体」のポインタを管理する。

Asyncifyが自動生成・エクスポートする主なAPIは以下の通り:

  • asyncify_start_unwind(ptr):巻き戻しを開始する。ptrは保存用データ構造体の先頭アドレス。
  • asyncify_stop_unwind():巻き戻しを終了する。
  • asyncify_start_rewind(ptr):再構築を開始する。ptrは保存用データ構造体の先頭アドレス。
  • asyncify_stop_rewind():再構築を終了する。
  • asyncify_get_state():現在の状態を返す。

データ構造とメモリレイアウト

asyncify_start_unwindasyncify_start_rewindに渡すポインタは、Wasmのリニアメモリ上の構造体を指す必要がある。構造体のレイアウトは以下の通り:

  • オフセット0: i32(asyncify stackの開始アドレス)
  • オフセット4: i32(asyncify stackの終了アドレス)

この領域の直後に、実際のasyncify stack(状態保存用のメモリ領域)を確保する。スタックサイズが小さすぎるとunreachable例外が発生するため、十分なサイズを確保する必要がある。

2.3. コールスタックの巻き戻し・再構築

  • 一時停止したい箇所でasyncify_start_unwindを呼び出すと、Wasmランタイムはコールスタックとローカル変数の状態を保存しながら「巻き戻し」を開始する。
  • その後、asyncify_stop_unwindで巻き戻しを終了し、Wasmの実行が一時停止する。
  • 再開時はasyncify_start_rewindを呼び、保存された状態をもとに「再構築(リワインド)」を行う。
  • asyncify_stop_rewindで再構築を終了し、元の関数の続きから実行が再開される。

なぜ2回呼ぶ必要があるのか

巻き戻し・再構築の過程では、同じ関数が2回呼ばれることになる。1回目は通常実行、2回目はリワインドのため。関数内で「今が巻き戻し中か再構築中か」を判定し、適切な分岐を行う必要がある。

2.4. 例:純粋なWebAssemblyでの利用

(module
  (memory 1 1)
  (import "spectest" "print" (func $print (param i32)))
  (import "asyncify" "start_unwind" (func $asyncify_start_unwind (param i32)))
  (import "asyncify" "stop_unwind" (func $asyncify_stop_unwind))
  (import "asyncify" "start_rewind" (func $asyncify_start_rewind (param i32)))
  (import "asyncify" "stop_rewind" (func $asyncify_stop_rewind))
  (global $sleeping (mut i32) (i32.const 0))
  (start $runtime)
  (func $main
    (call $print (i32.const 1))
    (call $sleep)
    (call $print (i32.const 3))
  )
  (func $sleep
    (if
      (i32.eqz (global.get $sleeping))
      (block
        (global.set $sleeping (i32.const 1))
        (i32.store (i32.const 16) (i32.const 24))
        (i32.store (i32.const 20) (i32.const 1024))
        (call $asyncify_start_unwind (i32.const 16))
      )
      (block
        (call $asyncify_stop_rewind)
        (global.set $sleeping (i32.const 0))
      )
    )
  )
  (func $runtime
    (call $main)
    (call $asyncify_stop_unwind)
    (call $print (i32.const 2))
    (call $asyncify_start_rewind (i32.const 16))
    (call $main)
  )
)

この例では、mainを2回呼ぶことで、途中で一時停止し、外部から再開できるようになっている。

2.5. 最適化とオーバーヘッド

  • Asyncifyを有効にすると、Wasmバイナリのサイズは1.5~2倍程度になることが多い。
  • 実行速度も最大で2倍程度遅くなるが、実際に一時停止・再開を多用しない限り、オーバーヘッドは限定的。
  • importリスト(どのimportが一時停止を引き起こすか)を明示することで、不要な関数の変換を避け、サイズ・速度のオーバーヘッドを大幅に削減できる。
  • indirect call(間接呼び出し)が多い場合、静的解析が難しくなり、オーバーヘッドが増える。
  • 最適化時はwasm-opt -O1 --asyncifyのように、Asyncifyと同時に最適化パスを有効にすることが推奨される。

2.6. 注意点

  • asyncify stackのサイズが不足するとunreachable例外が発生するため、十分な領域を確保すること。
  • 最適化レベルによっては、関数のインライン展開などでAsyncifyの動作が乱れる場合がある。-O1程度が安全。
  • importリストやindirect callの扱いを適切に設定することで、オーバーヘッドを最小化できる。

(参考: Pause and Resume WebAssembly with Binaryen's Asyncify

3. 主なクラス・構造

クラス名 役割
ModuleAnalyzer どの関数が「状態を変える(アンワインド/リワインドを開始する)」可能性があるかを解析する
AsyncifyFlow 関数本体の制御フローを書き換え、「一時停止・再開」に対応できるようにする
AsyncifyLocals ローカル変数の保存・復元処理を挿入する
FakeGlobalHelper 一時的な値の保存用に「偽グローバル変数」を使う仕組みを提供する

4. 変換の例

例えば、

x = x + 1;
bar(x);
x = x / 2;

のようなコードがあった場合、

  • 「bar(x)」の呼び出しで一時停止する可能性があるなら、
  • その前後で「状態チェック」や「保存・復元処理」が自動的に挿入される。

5. オプション・カスタマイズ

  • どの関数・インポートが一時停止を引き起こすかをリストで指定できる。
  • 最適化レベルやアサート挿入、詳細なログ出力などもオプションで制御可能だ。

6. まとめ

Asyncify.cpp は、WebAssembly の関数を「途中で一時停止・再開できる」ように自動変換するパスである。

  • コールスタックやローカル変数の保存・復元
  • 状態管理用のグローバル変数APIの自動生成
  • 制御フローの書き換え
  • どの関数を変換するかの解析

などを行い、C/C++ などの「同期的なコード」を JS などの「非同期イベント駆動環境」でも自然に動かせるようにする。