Scala の Wasm バックエンドを実装した
Scala.js 1.17.0 で実験的な Wasm backend がサポートされました!
リリースノートに書いてあるとおり、以下のような設定をすることでScala.jsがJSの代わりにWasmモジュール(とモジュールに渡すJS object)を生成することができます。
@JSExport
によるモジュールのexportがサポートされていませんが、それ以外のsemanticsはサポートされており、既存のScala.jsアプリケーションを変更なしにWasmにビルドすることが可能なはずです。(もし何か問題があれば教えて下さい!)
// Emit ES modules with the Wasm backend
scalaJSLinkerConfig := {
scalaJSLinkerConfig.value
.withExperimentalUseWebAssembly(true) // use the Wasm backend
.withModuleKind(ModuleKind.ESModule) // required by the Wasm backend
.withPrettyPrint(true) // (デバッグ用) これを設定すると .wat ファイルも一緒に生成してくれる
},
// Configure Node.js (at least v22) to support the required Wasm features
jsEnv := {
val config = NodeJSEnv.Config()
.withArgs(List(
"--experimental-wasm-exnref", // required
"--experimental-wasm-imported-strings", // optional (good for performance)
"--turboshaft-wasm", // optional, but significantly increases stability
))
new NodeJSEnv(config)
},
以下の Wasm features を利用した Wasm module を生成しているため、新し目のブラウザ・JSランタイムが必要になります(Node.js v22 など)
- Wasm Garbage Collection
- Wasm Exception Handling
- (optional) JS String Builtin
- 利用できない場合はpollyfillによる実装にfallbackします
- Tail Call Extension
各種ブラウザ・JSランタイムのサポート状況は https://webassembly.org/features/
またJS環境で実行可能なWasmしか生成することができず、wasmtimeやWasmEdgeといったWasmランタイムはサポートしていません。
パフォーマンス・バイナリサイズ
Wasmは早いとか遅いとか言われますが、実際はWasmは最適化する余地がJSより大きいというだけの話(という理解)で早いか遅いかはアプリケーションの性質とコンパイラとVMの実装にかかっています。残念ながら、現時点ではWasmバックエンドは必ずしもScala.jsのJSバックエンドより実行速度が早いコードを生成するとは限りません。
また生成されるコードのサイズも、現時点では Scala.js の JS backend + Google Closure Compiler を使って最適化したものの方が、Wasm backend よりも小さいコードを生成します。
少し前のデータになりますが、以下のブログ(英語)にベンチマーク結果を書いていますので詳しく知りたい方はそちらを参照してください。
また以下のツイートではJSバックエンドとWasmバックエンドで生成したLife of Gameの実行速度を比較していますが、やはりJSの方が幾分早いですね。
単純な数値計算なんかはWasmが長じているのですが、JS interopが多く発生するとそのオーバーヘッドがパフォーマンスに大きく影響してくるように感じています。
これを見てWasm遅いじゃん!と思ってほしくはなくてScala.jsは10年以上の最適化の積み重ねにより効率的なJSコードを生成している(VMも同じですね)のに対して、Wasmバックエンドは開発開始からまだ約半年。まだまだ最適化の余地が多く残されています。
例えばまだwasm-optは一部のWasmGCの機能が不足しているため、Scala.jsが生成したWasmバイナリに対して最適化を実行することができません。(block parameter typeをlocal.get
とlocal.set
にlowerするパッチは手元にあるのですが...)
今後の最適化でどのくらい高速にバイナリサイズを小さく出来るか楽しみですね。
今後
- さらなる最適化
- JS依存のないWasmモジュールの生成。これによりwasmtimeやwasmedgeなどのstand-alone wasm runtimeで実行できるコードを生成
- Wasm Component Model (host/guest) 対応
- WASI Preview 2 対応のためにどちらにしろhostとしての対応は必要になる
- GCをmoduleに組み込まず、WasmGCを利用する選択をしたからこそサイズの小さいguest componentが作れるのではないか?
-
stack swtiching proposalによる効率的なVirtual Threadの実装
- ScalaJVMはProject Loom、ScalaNativeはsetjmp/longjmpベースのdelimccによりVirtual Thread APIをサポートしていますが、ScalaJSではwhole program transformationをするしか術がありませんでした。stack switching proposal により ScalaJS でも効率的な Virtual Thread API の実装が可能になるのではないかと期待しています(まだ全然調べてない)
感想
2024年初め頃にScala.jsのWasmバックエンド実装を始めて、やっとこさアップストリームへのリリースまでこぎつけることができました。最初のベース実装は僕がやったのですが、そこから実際にあらゆるソフトウェアを動かすことができるようにするまではScala.js作者の@sjrdがかなり実装してくれました(ありがとう)。
引き続き、ScalaのWasm対応頑張っていこうと思います。
Discussion