Info
本記事はAkatsuki Games Advent Calendar 2022における16日目の記事です。 15日目の記事はぐんそうさんの『新卒2年目 リーダーへの挑戦』でした。胸が熱くなるほどエモい記事でしたので、ご一読いただけますと幸いです。

記事の概要 見出しへのリンク

要約 見出しへのリンク

本記事の要約を以下に示します。

  • ゲーム本体からロジックを切り出して利用する場合、Luaがしばしば用いられている
    • 組み込みがしやすい、取り回しがよい(ロジックの保守性が向上する)などのメリットがある
    • スクリプト部分の差し替えが容易
  • ただし、Luaは速度面、記法面でデメリットがある
    • LuaJITかつJIT有効の環境でない限り、基本的に速度は遅い
      • fib(42) の計算時間を例に
    • 記法もやや独特であり、言語としての書きやすさには疑問符がつく
  • 本記事では、代替としてWASI (WebAssembly)を利用することを提案する
    • 高速で、単一の特定言語に縛られない(WASI対応言語である限り、選択肢がある)
    • Lua同様マルチプラットフォームで、Unityからブラウザまで広い環境で動作する
      • Luaとの比較をして動作例を示す
        • JIT無効の場合、最大で40〜50倍ほどWASIの方が高速
  • 一方、WASIは発展途上の技術という点が最大のデメリット
    • 実績不足や仕様面での不足がある
      • 解消される見込みはある
      • 今後に期待
    • また、Luaと比較してWASI (wasm)へのコンパイルの手間がある
      • wasm へのコンパイル時間は言語に依存
      • wasmu へのコンパイルはCraneliftを使う限り高速

サンプルプロジェクト 見出しへのリンク

本記事で用いるコード・プロジェクト一式は以下で公開しています。

  • GitHubリポジトリ1
    • xLuaなどサードパーティのライブラリは含みません。
    • コンパイル済みファイルは含みません。すべてソースコードのみでの提供になります。

はじめに 見出しへのリンク

Luaによるロジック切り出し 見出しへのリンク

ゲーム開発において、ロジック切り出しは一つのテーマです。 現在、多くのゲームはUnreal Engine2やUnity3といったゲームエンジンを用いて開発されています。ゲームエンジンを用いることで、特定の言語を使って実装を行い、さまざまなプラットフォーム向けにビルドすることが可能です。実装の大部分は単一の言語で共通化できると言っても過言ではありません。

とはいえ、例外はつきものです。Unityでロールプレイングゲームを作る場合を考えてみましょう。基本的なゲームシステムはUnity標準のC#で手堅く実装可能ですし、問題はありません。 しかし、会話劇や個々のバトルなどをC#4で定義・実装するわけにはいきません。会話劇や個々のバトルを全てC#にしてしまうと、うち一つを修正するだけでも長いコンパイルが走り開発効率が悪化するためです。さらに、配布形態(ストアなど)によってはバイナリの修正をリリースするのに審査含め数営業日を要します。誤字脱字やパラメータ修正のためだけに審査を踏むのは無駄が大きいものです。そういったものはパッチ等で随時差し替え可能な、アプリのバイナリとは別の部分で定義・実装することが望ましいと言えます。つまり、ロジックの切り出しが必要になるわけです。

対応する最も単純な実装としては、パラメータやデータをテキストやJSONなどにまとめておき、それをC#で記述した実装、いわゆるスクリプトエンジンで読み出すことが考えられます。しかし、これは車輪の再発明に近く「会話劇で条件分岐をしたい」という要件のために、評価式のパーサーから実装を検討するようなものです。そういう面倒な実装は既存のものを使って、APIの定義やロジックの中身の開発に注力したいと考えるのが一般的でしょう。

そのため、こうしたロジックの共通化にはしばしばLua5が用いられます。Luaは組み込みが容易なスクリプト言語で取り回しがよいのが特徴です。Luaに会話劇やバトルのロジックを記述することで、式の評価などの低レイヤーな部分は既存の実装に任せることが可能になり、開発者はロジックの中身に注力することが可能になります。また、スクリプトは本体と切り離されているので、パッチ等で後から差し替えることも容易です。修正ごとに長いコンパイルが走る心配もありません。

Luaの弱点 見出しへのリンク

処理速度 見出しへのリンク

一方で、Luaには欠点があります。よく挙げられるのは、処理速度と記法でしょう。 まず、処理速度について触れます。Luaは組み込みやすい反面、処理速度は速いといえません。 例えば、フィボナッチ数を計算するコードで速度を測定してみましょう。ある程度の処理負荷になるよう、今回はフィボナッチ数列の42番目を求めます。Luaとの比較対象としてはRust6を用いました。

function fib(n)
  if n == 0 then
    return 0
  elseif n == 1 then
    return 1
  else
    return fib(n - 2) + fib(n - 1)
  end
end

N = 42

print(string.format("fib(%d) is %d", N, fib(N)))
fib(42)を求めるLuaのコード
fn fib(n: i32) -> i32 {
    return match n {
        0 => 0,
        1 => 1,
        _ => fib(n - 2) + fib(n - 1)
    }
}

const N: i32 = 42;

fn main() {
    println!("fib({}) is {}", N, fib(42));
}
fib(42)を求めるRustのコード

計測環境は、2021年モデルのMacBook Pro (M1)7です。計測にはhyperfine8を用い、3回実行した結果の平均所要時間を求めます。 これらの実行結果は以下の通りです。Rust実装を理論値(ネイティブ)と置いています。念のため、高速なLua実装として知られるLuaJIT9も比較に加えました。

  • Rust v1.65.0 ( -C opt-level=3 -C debug_assertions=no -C lto=yes )
    • Time (mean ± σ): 853.2 ms ± 1.3 ms [User: 843.1 ms, System: 2.1 ms]
  • Lua 5.4.4
    • Time (mean ± σ): 23.638 s ± 0.261 s [User: 23.557 s, System: 0.058 s]
  • LuaJIT 2.1.0-beta3 ( JIT有効 )
    • Time (mean ± σ): 1.826 s ± 0.006 s [User: 1.813 s, System: 0.004 s]
  • LuaJIT 2.1.0-beta3 ( JIT無効 )
    • Time (mean ± σ): 15.874 s ± 0.020 s [User: 15.820 s, System: 0.035 s]

この結果から分かる通り、少なくとも素のLuaの処理速度は極めて遅いことが分かります。JIT有効のLuaJITにしても、ネイティブと比較し2倍以上の時間を要しています。

文法 見出しへのリンク

もう一つの弱点としては、Luaの文法、仕様が挙げられます。配列(テーブル)の1オリジンなどは有名でしょうか。

array = { "one", "two", "three" }
print(array[0]) -- nil
print(array[1]) -- "one"

さらに、否定の条件も一般的な != ではなく ~= という独特な記法です。

a = 10
if a != 0 then print("error") end -- これはエラーになる
if a ~= 0 then print("ok") end -- "ok"

switch-caseのような文もありませんし、型付けもできません。小さなプログラムを書くのにはよくても、ある程度大きくなってくると処理を追うだけも苦労を伴います。

WASI (WebAssembly)という選択肢 見出しへのリンク

WASIとは 見出しへのリンク

Luaは柔軟な反面、このようないくつかの弱点を抱えていることが分かりました。 そこで、今回はこのようなロジックの切り出しに、近年注目度が高まってきているWASIを用いてみることにします。

WASI10は、WebAssembly11にPOSIX12に似たインターフェースを持たせるための規格です。 WebAssemblyはもともとブラウザ向けの技術だったため、入出力などを扱うには自前でグルーコードを書く必要がありました。ブラウザとその他で同一のWebAssemblyを実行したいと思った場合、関数など諸々呼び出しの仕様を決めなくてはいけません。 WASIを用いると、こうした面倒な点をスキップできます。普通のCLIアプリケーションと全く同じコードをWebAssemblyとしてコンパイル・動作させることが可能です。

例えば、先ほど示したフィボナッチ数を求めるRustのコードは、変更を一切加えることなくWASIのWebAssemblyにコンパイルできます。

$ rustc -C opt-level=3 -C debug_assertions=no -C lto=yes --target wasm32-wasi fib.rs -o fib.wasm

出力された wasm ファイルは、WASI対応の任意のランタイムで実行可能です。代表的なランタイムであるWasmer13とWasmtime14で実行してみます。

$ wasmer run fib.wasm
fib(42) is 267914296
$ wasmtime run fib.wasm
fib(42) is 267914296

ご覧の通り、標準出力も当然のように利用できていることが分かります。もちろん、引数なども利用可能です。 WASIのランタイムは様々ですが、中でもRustで書かれているWasmerはスマートフォンやブラウザへの組み込みにも対応しており15 16、これを使うと簡単にマルチプラットフォームでWASI (WebAssembly)を実行できるようになります。ライセンスもMIT Licenseと寛容であり、Luaと同様に様々な環境へ組み込むことができるわけです。

WASI (WebAssembly)の性能 見出しへのリンク

肝心の性能について見ていきます。実行するコードは先ほどコンパイルした fib.wasm を、ランタイムにはWasmer 3.0.2を選びました。 また、Wasmerは通常だとJITコンパイルによりWebAssemblyを実行しますが、事前にネイティブコード wasmu へ変換(AoTコンパイル)しておくことで、JIT非対応の環境に対応させることもできます。これについても、比較対象に加えることとします。

結果は以下のようになりました。

  • (再掲)Rust v1.65.0 ( -C opt-level=3 -C debug_assertions=no -C lto=yes )
    • Time (mean ± σ): 853.2 ms ± 1.3 ms [User: 843.1 ms, System: 2.1 ms]
  • Wasmer 3.0.2 ( JIT, Craneliftコンパイラ, fib.wasm を実行 )
    • Time (mean ± σ): 948.0 ms ± 3.3 ms [User: 932.9 ms, System: 3.8 ms]
  • Wasmer 3.0.2 ( AoT, Craneliftコンパイラ, wasmer compile fib.wasm -o fib.wasmu で得られた fib.wasmu を実行 )
    • Time (mean ± σ): 939.9 ms ± 4.7 ms [User: 929.0 ms, System: 4.1 ms]

AoTコンパイルを行った fib.wasmu の方が高速なのは自明ですが、ほぼ誤差です。いずれにせよ、LuaJITに対しおよそ半分ほどの処理時間で済んでいることが分かります17

WASIにおける言語選択の自由度 見出しへのリンク

ここまで機能や性能について見てきましたが、WASIにはさらに利点があります。 WASIは既に述べた通り規格でしかありません。つまり、言語がWASIに対応さえしていれば、どの言語で記述しても同じ機能を持つWASIを生成できるのです。

例えば、先ほどのRustのフィボナッチのコードは、以下のGolangコードへ簡単に置き換えることができます。

package main

import (
	"fmt"
)

func fib(n int32) int32 {
	switch n {
	case 0:
		return 0
	case 1:
		return 1
	default:
		return fib(n-2) + fib(n-1)
	}
}

const N int32 = 42

func main() {
	fmt.Printf("fib(%d) is %d\n", N, fib(N))
}
fib(42)を求めるGolangのコード

このコードをTinyGoでWASIのwasmとして出力して実行してみましょう。

$ tinygo build -wasm-abi=generic -target=wasi -o fib.wasm fib.go
$ wasmer run fib.wasm
fib(42) is 267914296
$ time wasmer run fib.wasm > /dev/null  
wasmer run fib.wasm > /dev/null  0.94s user 0.01s system 88% cpu 1.075 total

処理時間もさほど変動せず、結果も一致しています。ランタイムを維持したままロジックの言語・実装だけを差し替えることができました。

Unityへの組み込み 見出しへのリンク

ここまで、WASIのメリットについて説明しました。では、ゲームで使うことを想定して実際にUnityへと組み込んでみましょう。

ただし、先に示したフィボナッチ数を求めるコードでは視覚的な効果が得られないため、Unityに移植しても面白くありません。そこで、マンデルブロ集合18のPBM画像をUnityのC#とは別に切り出したロジックで生成することにします。

WASIもLua同様に様々な環境で組み込み・実行できることを示すため、実行対象のプラットフォームは以下の通り複数選定しました19

  • Unity Editor (M1 Mac, Apple Silicon版)
  • Android (ARM64)
  • iOS (ARM64)

WASI (WebAssembly)コードの生成 見出しへのリンク

動作確認と比較のために、LuaとRustのコードをそれぞれ用意します。実装の正確性についての議論を避けるため、Debianが提供しているBenchmark Games20のコードを借用しました。ただ、実装内容・アプローチに差がみられることから、厳密な速度比較は諦めることにします。

また、両者について組み込みの際に不都合が生じたので、それぞれ軽微な変更を加えました。以下にコードと変更点を示します。

  • Benchmarks Gameの mandelbrot Lua #3 21から借用し、xLuaで扱うことを前提に変数へ結果を格納するよう修正。3条項BSDライセンス22

https://github.com/flfymoss/202212-ac-wasi-sample/blob/release/mandelbrot/lua/main.lua

  • Benchmarks Gameの mandelbrot Rust #6 23から借用し、マルチスレッド対応を削除。3条項BSDライセンス22

https://github.com/flfymoss/202212-ac-wasi-sample/blob/release/mandelbrot/rust/src/main.rs

このうち、Rustのコードは以下のコマンドで wasm へとコンパイルしておきます。

$ rustc -C opt-level=3 -C debug_assertions=no -C lto=yes --target wasm32-wasi mandelbrot.rs -o mandelbrot.wasm
# または、Cargo用プロジェクトを作成した状態で
$ cargo build --release --target wasm32-wasi

さて、ここまででWASIの wasm ファイルの準備ができました。しかし、これはバイトコードに過ぎないため、実行するには機械語へのコンパイルが必要です。 とはいえ、WasmerはデフォルトでCraneliftと呼ばれるコンパイラを内蔵しているので、 wasm ファイルをJITコンパイルによりそのまま実行することができます。 ただし、ゲームを実行するプラットフォームによっては、セキュリティ上の都合などからJITコンパイルに対応していないこともしばしばです。例えば、iOSはJITコンパイルを認めていません。つまり、AoTコンパイルが必要になります。

折角ですから、今回はiOS対応も兼ねて、AoTコンパイルしたネイティブコードである wasmu ファイルも生成してみましょう。WasmerのAPIを利用し、 wasmu の生成を目指します。

Warning

通常であれば wasmer compile コマンドで簡単に wasmu ファイルを生成できるのですが、今回は利用しません。 Wasmer 3.0.2で試したところ、コマンドから直接生成された wasmu のコードではmmapが適切に処理されない24ためです。 調査の結果、(おそらく)実装に不備があることが分かりました25 26。そのため、コンパイラのAPIを直接呼び出し、以下のコード中にある Dirty hack を行って問題を回避するようにしています。

Cargoプロジェクト全体はサンプルリポジトリ27を参照いただくとして、具体的にコンパイラAPIを呼び出すコードは以下のようになります。

https://github.com/flfymoss/202212-ac-wasi-sample/blob/release/compiler/src/main.rs

Cargoプロジェクトが準備できたら、以下のコマンドでビルドしましょう。

# プロジェクトのディレクトリで実行
$ cargo build --release

ビルドしてできたコンパイラを用いて、以下のように wasmwasmu へとコンパイルします。 生成された wasmu ファイルは、iOSはもちろん、JITコンパイルに対応した環境でも実行することができます。

$ ./target/release/my-compiler ../mandelbrot.wasm wasmu/mandelbrot
$ ls wasmu
mandelbrot.android.wasmu	mandelbrot.arm64.wasmu		mandelbrot.ios.wasmu

これで、JITをサポートしないiOSのような環境でもWASIを動作させる準備が整いました。

Wasmer(WASIランタイム)の組み込み 見出しへのリンク

Unityで wasmwasmu を実行させるため、Wasmerをネイティブプラグインとして組み込みます。 WasmerはRustで書かれているので、Rustを使ってネイティブプラグインをビルドしてみましょう。Cargoプロジェクト全体はサンプルリポジトリ28を参照いただくとして、プラグインの実コードは以下のようになります。

https://github.com/flfymoss/202212-ac-wasi-sample/blob/release/loader/src/lib.rs

Cargoプロジェクトが準備できたら、以下のコマンドでビルドしましょう。ビルドにはTripleの指定が必要です。

# プロジェクトのディレクトリで実行
# wasmu用 Wasmer(headless)
# M1 Mac 向けプラグインを作成
$ cargo build --release --lib
# Android (ARM64) 向けプラグインを作成
$ cargo build --release --lib --target aarch64-linux-android
# iOS (ARM64) 向けプラグインを作成
$ cargo build --release --lib --target aarch64-apple-ios
# wasm用 Wasmer(Cranelift)
# M1 Mac 向けプラグインを作成
cargo build --release --lib --features wasi_jit --target-dir target_jit
# Android (ARM64) 向けプラグインを作成
cargo build --release --lib --features wasi_jit --target-dir target_jit --target aarch64-linux-android
# iOS はJIT非対応なので割愛
ビルドするためのコマンド

ビルドが完了したら、後述するUnityプロジェクトの Assets/Plugins/(アーキテクチャ名) 配下にビルドしたプラグインのファイルを配置する必要があります。 例えば、M1 Mac, Android, iOS向けのファイルの配置は以下のようになります。

  • Assets/Plugins
    • Android
      • libs
        • arm64-v8a
          • libloader.so
          • libloader_jit.so
    • arm64
      • libloader.dylib
      • libloader_jit.dylib
    • iOS
      • libloader.a
        バイナリの配置

WASI (WebAssembly)の実行用C#コードの実装 見出しへのリンク

WASIを扱う部分は完成したので、Unityからそれを実行する部分を実装しましょう。 Unityは2022年12月12日時点で最新のLTSである、2021.3.15f (Apple Silicon版)を利用しました。

比較が分かりやすいように、実行時間を計測して表示する機能も入れておきます。Unityプロジェクト全体はサンプルリポジトリを参照いただくとして29、C#のコードは以下の通りです。

https://github.com/flfymoss/202212-ac-wasi-sample/blob/release/unity/mandelbrot/Assets/MandelbrotController.cs

コード中にもあるように、 Assets/StreamingAssets 以下にここまでの節で作成した .lua, .wasm, .wasmu ファイルの格納が必要です。配置先ディレクトリをコードに沿って適宜作成の上、ファイルをコピーしてください。

また、比較用のLuaの実行のためにxLua v2.1.16を用いています。xLuaのリリースページ30からダウンロードの上、ドキュメントに従って組み込んでください。

各プラットフォームで実行してみる 見出しへのリンク

ようやく実行できるようになったので、早速試してみましょう。 対象とするプラットフォームは Unityへの組み込み の節で示した通り、Unity Editor (M1 Mac, Apple Silicon版)、Android (ARM64)、iOS(ARM64)です。

Warning

なお、Unityでの実行比較では先に示したmandelbrotのコードを用いていますが、以下の理由から速度は参考程度とし、正確な計測は行いません。

  • LuaとRustのコードが、それぞれ比較に耐えうる形で平等に実装されていることが保証できないため
  • xLuaとWasmerのランタイムが、それぞれ比較に耐えうるほど平等な形で組み込まれていることが保証できないため

比較用に以下のランタイムを組み込みます。

  • xLua v2.1.16 (LuaJIT)
  • Wasmer 3.0.2 ( wasmu を実行)
  • Wasmer 3.0.2 + Cranelift ( wasm を実行) 31

生成するマンデルブロ集合の画像は、正方形画像の一辺N(px)を以下3パターンで設定し、それぞれ3回ずつ生成した際の平均実行時間を測定します。

  • N = 120
  • N = 1000
  • N = 4000

Unity Editor 見出しへのリンク

まず、Unity Editorで試します。以下の動画は実行した結果を2.5倍速にしたものです。

mandelbrot on Unity Editor

mandelbrot on Unity Editor

実行に要した平均時間は以下のようになります。

  • N = 120
    • xLua : 15.7ms
    • Wasmer (wasmu) : 6.7ms
    • Wasmer (wasm) : 40.3ms
  • N = 1000
    • xLua : 788.0ms
    • Wasmer (wasmu) : 23.0ms
    • Wasmer (wasm) : 49.3ms
  • N = 4000
    • xLua : 12547.3ms
    • Wasmer (wasmu) : 327.3ms
    • Wasmer (wasm) : 344.3ms
      実行に要した時間一覧(3回の平均)

xLuaがかなり遅いですが、これはM1 Mac用のxLuaバイナリ(LuaJIT)でおそらくJITが有効になっていないことが原因と考えられます。 JITコンパイルの wasm は N = 120 のような小さな処理には向きませんが、 N = 4000 のように重い処理では wasmu に近い速度で処理ができていることが分かります。

Android (ARM64 エミュレータ) 見出しへのリンク

次に、Androidで試します。今回はAndroid 13のエミュレータ上で実行しました。以下の動画は実行した結果を2.5倍速にしたものです。

mandelbrot on Android

mandelbrot on Android

実行に要した平均時間は以下のようになります。

  • N = 120
    • xLua : 4.7ms
    • Wasmer (wasmu) : 14.3ms
    • Wasmer (wasm) : 50.0ms
  • N = 1000
    • xLua : 100.3ms
    • Wasmer (wasmu) : 27.0ms
    • Wasmer (wasm) : 51.7ms
  • N = 4000
    • xLua : 1402.0ms
    • Wasmer (wasmu) : 346.3ms
    • Wasmer (wasm) : 367.3ms
      実行に要した時間一覧(3回の平均)

Unity Editorでの結果と違いxLuaが明らかに高速なため、JIT有効のLuaJITが利用されているようです。 Nが小さい時は wasm よりもxLuaが優位ですが、N が上がるにつれて wasm が逆転し、 N = 4000 ではおよそ3倍ほど wasm の方が高速になることが示されました。

iOS (iPhone SE 2) 見出しへのリンク

最後に、iOSで試します。こちらはiPhone SE 2 (iOS 15.3.1) の実機上で実行しました。また、iOS ではJITコンパイルが認められていないため、JIT無効のxLua (LuaJIT)と wasmu のみで比較します。 以下の動画は実行した結果を2.5倍速にしたものです。

mandelbrot on iOS

mandelbrot on iOS

実行に要した平均時間は以下のようになります。

  • N = 120
    • xLua : 44.3ms
    • Wasmer (wasmu) : 17.7ms
  • N = 1000
    • xLua : 1047.3ms
    • Wasmer (wasmu) : 75.3ms
  • N = 4000
    • xLua : 16067.3ms
    • Wasmer (wasmu) : 497.7ms
      実行に要した時間一覧(3回の平均)

ネイティブの wasmu とJIT無効のインタプリタでの速度差が出るのは自明ですが、JIT禁止の環境であれば、WASIが圧倒的に有利であることが分かります。

結果の考察 見出しへのリンク

これら3プラットフォームの結果から、以下のことが分かりました。

  • wasm は短時間で終わる処理に不向きで、その場合xLua (LuaJIT)の方が速い
    • 処理が重くなるにつれて逆転し、 wasmu と同程度の速度まで漸近する
    • 従って、JITコンパイルがボトルネックになっているものとみられる
  • wasmu は全プラットフォーム、全パターンを通して極めて高速
    • ただし、JIT有効の環境かつ短時間で終わる処理については、LuaJITに負けている点に注意が必要
      • Wasmerの初期化コストがボトルネックになっていると考えられる
  • xLuaでLuaJITを用いる場合、プラットフォームによって速度が明らかに異なる
    • JIT有効の環境であれば、悪い選択肢ではない
    • JIT無効の環境では、短時間で終わる処理以外全く向かない

ある程度重い処理を切り出して実行する場合は、WASIを選択した際のメリットが大きいと言えます。JIT無効の環境では事実上の無双状態です。Unity EditorやiOSの結果において、xLuaと wasmu の差は30倍以上、実測で10秒以上も差が生まれている点は特筆すべき点でしょう。 一方で軽い処理しか切り出して実行しない場合、WASIによる高速化はわずかで、コンパイルの手間を踏まえるとメリットが大きいとまでは言えないでしょう。JIT有効かつN = 120の環境下ではLuaJITの方が wasmu よりも高速である点も注意が必要です。

ブラウザ上での実行 見出しへのリンク

ここまで、WASIとして切り出したロジックをUnityを使う例を見てきました。 ですが、既に述べた通り、WASIはブラウザでも実行することができます。ブラウザゲームで効果を発揮することはもちろん、ゲーム開発用の管理画面などでロジックを直接実行し、素早くデバッグ・調整を行うことも可能になります。

マルチプラットフォーム対応の大詰めとして、マンデルブロ集合の wasm をブラウザ上でも実行できるようにしてみます。コードはHTMLやJavaScriptに散らばっているので、サンプルリポジトリ32を参照ください。

コードを用意したら、同じディレクトリ内に mandelbrot.wasm をコピーしておきます。完了したら、以下のコマンドで開発用のローカルサーバーを立ち上げてみましょう33

# Node.jsがインストールされていることが前提。なければ適宜インストールする
$ npm install
$ npx vite
  VITE v3.2.4  ready in 106 ms

  ➜  Local:   http://127.0.0.1:5173/
  ➜  Network: use --host to expose

Localの部分のURLにアクセスするとマンデルブロ集合の生成ページが表示されるはずです。Google Chrome34で閲覧し、Unityと同様に実行した例を以下に示します。

mandelbrot on web

mandelbrot on web

ブラウザの実行エンジンで動作するためUnityの結果とは単純比較できませんが、実行時間の平均を以下に示します。

  • N = 120
    • 14.7ms
  • N = 1000
    • 216.0ms
  • N = 4000
    • 3140.7ms
      実行に要した時間一覧(3回の平均)

Unityでの計測結果よりかなり遅いものの35、JIT無効のxLua (LuaJIT)よりは高速に動作していることが分かります。

課題・懸念点 見出しへのリンク

さて、ここまではWASIの強みや動作例を示してきましたが、もちろん弱点も存在します。

まず、WASIには十分な実績がありません。WebAssembly自体はかなり広まりましたが、WASIは広がっていると言い難い状況です。 また、機能面での不足が多数あります。スレッド対応などの重要な機能すら仕様策定中であることは、WASIの大きな弱点でしょう36。ただし、例えばWasmerでは策定中の機能らを実装した標準ライブラリ (WASIX libc37)が提案されていたりと、改善の兆しは見えます。

最後に、Luaとは異なり、 wasm へのコンパイルが最低一回は必要です。コンパイル時間は言語に依存しますが、規模が大きなコードをコンパイルする場合は時間がかかるかもしれません。 ただし、AoT/JITコンパイルはCraneliftを使う限り高速であり、さほど問題にはならなさそうに見えます。JIT Bomb38対策としてSinglepassコンパイラも提供されているので、 wasmu へのコンパイル時間が問題になることは少ないでしょう39

まとめ 見出しへのリンク

本記事では、まず、ゲームのロジックの切り出しにLuaが用いられる背景を説明しました。 次に、Luaの弱点を示し、代替としてWASI (WebAssembly)の利用を提案しました。また、各プラットフォームにおけるUnity上での Lua (xLua) と WASI (Wasmer) の動作比較から、Luaの弱点をWASIが一定克服できることを示しました。 最後に、WASIは発展途上であり十分な実績がないこと、仕様として不足している機能があること、Luaと比較しコンパイルの手間があることを説明しました。

WASIは発展途上の技術ですが、仕様からランタイム、言語の対応まで、多くの関連技術が日々進化を続けています。今後のさらなる発展にも期待が持てるでしょう。

謝辞 見出しへのリンク

本記事を執筆するにあたり、以下の方にご協力いただきました。この場をお借りして厚くお礼申し上げます。

  • @cllightz さん
    • iOSでの動作検証に際して、助言・確認をいただきました。ありがとうございました。
  • 🍊
    • 内容の添削をしていただきました。ありがとうございました。

ライセンス 見出しへのリンク

本記事の内容は、特記なき限りCreative Commons Attribution 4.0 International Public License40のもとで自由に利用することができます。ただし、別のライセンスが示されている部分についてはそちらに従ってください。 また、Zennにて同一の内容41を掲載しています。


Info
Akatsuki Advent Calendar 2022 、明日はYuji Sugiyamaさんの『画像から音楽を生成する「img2music」を手探ってみる』が公開されます。昨今のAIの進歩には目を見張るものがありますが、どんな内容になるのかが楽しみです。乞うご期待。

  1. https://github.com/flfymoss/202212-ac-wasi-sample ↩︎

  2. https://www.unrealengine.com/ ↩︎

  3. https://unity.com/ ↩︎

  4. https://learn.microsoft.com/ja-jp/dotnet/csharp/ ↩︎

  5. https://www.lua.org/ ↩︎

  6. https://www.rust-lang.org/ ↩︎

  7. MacBook Pro(16インチ、2021), macOS Monterey 12.6.1 ↩︎

  8. https://github.com/sharkdp/hyperfine ↩︎

  9. https://luajit.org/ ↩︎

  10. https://wasi.dev/ ↩︎

  11. https://webassembly.org/ ↩︎

  12. https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap01.html ↩︎

  13. https://wasmer.io/ ↩︎

  14. https://wasmtime.dev/ ↩︎

  15. https://github.com/wasmerio/wasmer ↩︎

  16. https://github.com/wasmerio/wasmer-js ↩︎

  17. 今回はCraneliftと呼ばれるWasmerデフォルトのコンパイラを用いましたが、WasmerはLLVMにも対応しており、より高速なコードを生成可能です。ただし、LLVMはコード生成にかなりの時間を要するため、Luaとの比較には相応しくないと判断し本記事からは除外しています。 ↩︎

  18. https://www1.econ.hit-u.ac.jp/kawahira/courses/mandel.pdf ↩︎

  19. x86_64での環境を今回は用意できませんでしたが、理論上は動作すると思われます(未検証)。 ↩︎

  20. https://benchmarksgame-team.pages.debian.net/benchmarksgame/index.html ↩︎

  21. https://benchmarksgame-team.pages.debian.net/benchmarksgame/program/mandelbrot-lua-3.html ↩︎

  22. https://benchmarksgame-team.pages.debian.net/benchmarksgame/license.html ↩︎ ↩︎

  23. https://benchmarksgame-team.pages.debian.net/benchmarksgame/program/mandelbrot-rust-6.html ↩︎

  24. 厳密には、巨大な領域を確保しようとしてしまい、iOSにおいて Cannot allocate memory となります。 ↩︎

  25. https://github.com/wasmerio/wasmer/blob/5287c4f1253f66f428066d35eef0825fe827cff3/lib/api/src/sys/tunables.rs#L45 ↩︎

  26. https://github.com/wasmerio/wasmer/blob/5287c4f1253f66f428066d35eef0825fe827cff3/lib/vm/src/memory.rs#L222-L228 ↩︎

  27. https://github.com/flfymoss/202212-ac-wasi-sample/tree/release/compiler ↩︎

  28. https://github.com/flfymoss/202212-ac-wasi-sample/blob/release/loader ↩︎

  29. https://github.com/flfymoss/202212-ac-wasi-sample/tree/release/unity/mandelbrot ↩︎

  30. https://github.com/Tencent/xLua/releases ↩︎

  31. JITコンパイルが認められていないiOSでは除外しています。 ↩︎

  32. https://github.com/flfymoss/202212-ac-wasi-sample/tree/release/mandelbrot/browser ↩︎

  33. 今回はプレビュー用にローカルサーバーを利用しましたが、ビルドして静的ファイルにしてしまえば、ローカルでのサーバー実行は不要になります。 ↩︎

  34. https://www.google.com/intl/ja_jp/chrome/ 。閲覧時のバージョンは 108.0.5359.98 。 ↩︎

  35. wasm の実行エンジンや描画コード等が異なるため、Unityでの結果との比較を単純に行うことはできません。高速化する余地が残されている可能性があります。 ↩︎

  36. https://github.com/WebAssembly/WASI/blob/main/Proposals.md ↩︎

  37. https://github.com/wasmerio/wasix-libc ↩︎

  38. https://logmi.jp/tech/articles/325635 ↩︎

  39. https://github.com/wasmerio/wasmer/tree/master/lib/compiler-singlepass ↩︎

  40. https://creativecommons.org/licenses/by/4.0/legalcode.ja ↩︎

  41. https://zenn.dev/flfymoss/articles/2022-12-16-unity-meets-wasi ↩︎