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 採用哈佛架構,產生的位元碼無法直接執行。整個流程遵循四步管線:
- 編譯與實例化:將位元碼傳遞給瀏覽器的 JavaScript 嵌入層,進行編譯與實例化。
- 連結:將產生的函式加入主 Wasm 模組的間接函式表。
- 分派:使用
call_indirectWasm 指令執行該函式。
為了支援此流程,專案使用 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 的專案)中已被證實為有效策略。