WATaBoy:将 Game Boy 指令 JIT 编译为 WebAssembly

WATaBoy:将 Game Boy 指令 JIT 编译为 WebAssembly

WATaBoy 是一个概念验证的 Game Boy 模拟器,采用 JIT‑to‑Wasm 架构。通过在运行时生成 WebAssembly 字节码,然后由浏览器的 JS 引擎编译成本地机器码,WATaBoy 实现了超越原生解释器的性能。

性能结果

JIT‑to‑Wasm 仿真在 CPU 密集型任务中优于基于 Wasm 的解释器和原生解释器。在模拟《宝可梦蓝》的基准测试中,JIT‑to‑Wasm 方法比原生解释器快约 1.2 倍,比运行在 Wasm 中的解释器快约 1.5 倍。

浏览器引擎比较

不同主流浏览器引擎的性能有所差异,Safari 在此实现上表现出最高的效率。虽然 JIT 并未针对任何特定引擎进行调优,但 Safari 的性能领先表明 iOS 上仅有 WebKit 的特性并不会本质上限制 JIT‑to‑Wasm 仿真的性能。

技术实现

WATaBoy 使用 “JIT‑to‑Wasm” 方法绕过 iOS 等平台的 JIT 限制,Apple 只允许在网页浏览器(通过 JavaScriptCore/WebKit)内部进行 JIT 编译。模拟器不直接生成本地机器码,而是生成 Wasm 字节码,由浏览器进一步编译成本地代码。

Rust 中的 Wasm 代码生成与链接

实现使用 Rust 编写,并利用 wasm-encoder crate 输出 Wasm 指令。由于 Wasm 采用哈佛架构,生成的字节码不能直接执行。整个过程遵循四步流水线:

  1. 编译并实例化:将字节码交给浏览器的 JavaScript 嵌入层进行编译和实例化。
  2. 链接:将生成的函数加入主 Wasm 模块的间接函数表。
  3. 分发:使用 call_indirect Wasm 指令执行该函数。

为实现上述流程,项目使用 Nightly Rust 并开启 asm_experimental_arch 特性以支持内联 Wasm,同时传递特定的 LLD 标志(--export-table--growable-table),使 JavaScript 能访问并扩展间接函数表。

周期精度与 JIT 策略

为了在使用 JIT 的同时保持周期精度,WATaBoy 采用了受 GameRoy 启发的技术:

  • 中断预测:预测中断何时发生,以决定 JIT 块何时必须结束。
  • 解释器回退:当 JIT 块可能被中断时回退到解释器。
  • 惰性求值:对通过内存映射 I/O 访问的非 CPU 组件(如 MMIO)进行惰性求值。

限制与未来工作

虽然 JIT‑to‑Wasm 方法已取得成功,但仍存在若干限制:

  • 代码生成易用性:当前的 Wasm 字节码生成方式是定制的,缺乏像 DynASM 或 Cranelift 那样成熟的工具链支持。
  • 内存约束:某些底层优化(例如 Dolphin 的 “fastmem” 映射)在 Wasm 中不可行,因为无效的内存访问是不可恢复的。
  • PPU 瓶颈:由于中断预测未实现,运行时仍有大量时间花在模拟像素处理单元(PPU)上。

社区见解

开发者的讨论表明,性能提升是可以预期的,因为原生解释器的开销远高于 Wasm 的开销。一位贡献者指出:

"WASM 开销约为 20%,解释器开销约为 1000%。"

其他开发者则指出,类似的方法——基于运行时数据对热点路径进行实时重新编译——已在静态重编译项目(如针对 NES 的项目)中被证明是有效的策略。


摘要: WATaBoy 演示了面向 WebAssembly(Wasm)的即时编译(JIT)器能够在 Game Boy 仿真中超越原生解释器,为在 iOS 等受限平台上实现高性能仿真提供了一条潜在路径。

标题: WATaBoy:将 Game Boy 指令 JIT 编译为 WebAssembly

Sources