WATaBoy: JIT-ing Game Boy Instructions to WebAssembly

WATaBoy: JIT-ing Game Boy Instructions to WebAssembly

WATaBoy は、JIT‑to‑Wasm アーキテクチャを実装した概念実証の Game Boy エミュレータです。実行時に WebAssembly バイトコードを生成し、ブラウザの JS エンジンがそれをネイティブ機械語にコンパイルすることで、WATaBoy はネイティブインタプリタを上回る性能を実現します。

Performance Results

JIT‑to‑Wasm エミュレーションは、CPU 集中型タスクにおいて Wasm ベースのインタプリタとネイティブインタプリタの両方を上回ります。ポケモンブルーのエミュレーションベンチマークでは、JIT‑to‑Wasm アプローチはネイティブインタプリタより約 1.2 倍、Wasm 上で動作するインタプリタより約 1.5 倍高速でした。

Browser Engine Comparison

主要ブラウザエンジン間で性能に差が見られ、Safari がこの実装に対して最も高い効率を示しました。JIT が特定のエンジン向けにチューニングされていないにもかかわらず、Safari の性能優位は iOS の WebKit のみの環境が JIT‑to‑Wasm エミュレーションの性能を本質的に制限しているわけではないことを示唆しています。

Technical Implementation

WATaBoy は「JIT‑to‑Wasm」アプローチを採用し、iOS のように Apple が JIT コンパイルをウェブブラウザ内(JavaScriptCore/WebKit)に限定しているプラットフォームの制約を回避します。ネイティブ機械語を直接生成する代わりに、エミュレータは Wasm バイトコードを生成し、ブラウザがそれをネイティブコードにコンパイルします。

Wasm Codegen and Linking in Rust

実装は Rust で書かれ、wasm-encoder クレートを使用して Wasm 命令を出力します。Wasm はハーバードアーキテクチャを採用しているため、生成されたバイトコードは直接実行できません。プロセスは以下の 4 段階パイプラインに従います。

  1. Compile & Instantiate: バイトコードをブラウザの JavaScript 埋め込み環境に渡し、コンパイルとインスタンス化を行います。
  2. Link: 生成された関数をメイン Wasm モジュールの間接関数テーブルに追加します。
  3. Dispatch: call_indirect Wasm 命令を用いて関数を実行します。

これを実現するため、プロジェクトは Nightly Rust と asm_experimental_arch 機能を使用してインライン Wasm を記述し、LLD フラグ(--export-table--growable-table)を指定して間接関数テーブルへのアクセスと拡張を JavaScript から可能にしています。

Cycle-Accuracy and JIT Strategy

サイクル精度を保ちつつ JIT を利用するため、WATaBoy は GameRoy に触発された以下の技術を採用しています。

  • Interrupt Prediction: 割り込みが発生するタイミングを予測し、JIT ブロックの終了点を決定します。
  • Interpreter Fallback: JIT ブロックが割り込みされる可能性がある場合はインタプリタにフォールバックします。
  • Lazy Evaluation: メモリマップド I/O を介してアクセスされる非 CPU コンポーネント(例: MMIO)を遅延評価します。

Limitations and Future Work

JIT‑to‑Wasm アプローチは成功していますが、いくつかの制限が残っています。

  • Codegen Ergonomics: 現在の Wasm バイトコード生成は独自実装で、DynASM や Cranelift のような成熟したツールチェーンが提供する便利さに欠けます。
  • Memory Constraints: Dolphin の「fastmem」マッピングのような低レベル最適化は、無効なメモリアクセスが回復不能になるため Wasm では実現できません。
  • PPU Bottlenecks: 割り込み予測が未実装のため、Pixel Processing Unit(PPU)のエミュレーションに依然として多くの実行時間が費やされています。

Community Insights

開発者間の議論では、ネイティブインタプリタのオーバーヘッドが Wasm のオーバーヘッドよりはるかに大きいため、性能向上が期待通りであると指摘されています。あるコントリビュータは次のように述べています。

"WASM overhead is about 20%, interpreter overhead is about 1000%."

他の開発者は、実行時データに基づいてホットパスをライブリコンパイルする手法が、NES を対象とした静的リコンパイルプロジェクトなどで実証済みの戦略であると指摘しています。

Sources