WebAssemblyをブラウザの外で動かすWasmerを触ってみた
はじめに
WebAssemblyをブラウザの外でも動かせるWasmerの1.0がリリースしたので試してみました。
Wasmerってなに?
WasmerはWASI(WebAssembly System Interface)とEmscriptenに準拠したWASMを実行できるランタイムです。類似のものにFastlyのLucetがあるそうです。通常WASMはブラウザの中で実行するわけですがそのコンパクトでセキュアな仕様に注目して、ブラウザの外のあらゆる環境で動かすためにI/Oへのセキュアなアクセス方法も含めて定義されたのがWASIです。これによってセキュアでユニバーサルなランタイムを作るのがゴールみたいですね。
とりあえず触る前のリリース記事とか見た感想としては 「それ、なんてJava (JVM)?」
とりあえずインストール
公式ドキュメントを参考にしながら進めてみます。
$ curl https://get.wasmer.io -sSfL | sh
$ source /root/.wasmer/wasmer.sh
$ wasmer --version
wasmer 1.0.0
つづいてWASMで作られたJSのシェルのQuickJSを動かしてみます。
$ curl -o qjs.wasm https://registry-cdn.wapm.io/contents/_/quickjs/0.0.3/build/qjs.wasm
$ wasmer run qjs.wasm
QuickJS - Type "\h" for help
qjs > 1 + 1
1 + 1
2
qjs > console.log("Hello, WASM");
console.log("Hello, WASM");
Hello, WASM
無事にJSが動作しましたね。今回は直接WASMファイルをダウンロードしましたが、WAPM
(WebAssembly Package Manager)というパッケージマネーががあるようなのでこちらを使うことでもインストールが出来ます。
$ wapm install quickjs
$ wapm run qjs
QuickJS - Type "\h" for help
qjs >
ちなみに探してみたらWASM版LuaとかWASM版Pythonもありました。
RustでコンパイルしたWASMを実行してみる
さてありもののパッケージは動きましたが、やはり自分で書いたプログラムを動かしてみたいです。
とりあえず処理系レベルで対応していてWASMの生成が簡単そうなRustで簡単なコードを書いてみます。何気に人生初のRustコードかもしれない。
まずはRustをインストールします。
$ apt install build-essential
$ curl https://sh.rustup.rs -sSf | sh
$ source $HOME/.cargo/env
$ rustc --version
rustc 1.49.0 (e1884a8e3 2020-12-29)
続いて以下のようなHello Worldなコードを書きます。
fn main(){
print!("Hello, Rust");
}
以下のようにコンパイルして実行してみます。
$ rustc hello.rs
$ ./hello
Hello, Rust
無事動きましたね。
さて、上記のコマンドでは実行環境であるLinuxのバイナリ(ELF)になっているのでWASMとして出力します。コンパイルオプションを以下のように変更します。
$ rustc --target wasm32-wasi hello.rs
error[E0463]: can't find crate for `std`
|
= note: the `wasm32-wasi` target may not be installed
error: aborting due to previous error
For more information about this error, try `rustc --explain E0463`.
エラーになりましたね? というのもターゲットに指定したwasm32-wasi
をインストールしていないためです。なので以下のコマンドでインストールして再度コンパイルします。
$ rustup target add wasm32-wasi
$ rustc --target wasm32-wasi hello.rs
$ ls -l hello.wasm
-rwxr-xr-x 1 koduki koduki 1819416 Jan 11 02:29 hello.wasm
無事コンパイルされましたね。このコードを実行してみます。
$ wasmer run hello.wasm
Hello, Rust
WASMとして実行できたのが確認できたと思います。
RustとWasmerの速度差を確認する
続い程度の程度の速度なのか比較をしてみます。
まずは起動時間のチェック。
$ time ./hello
Hello, Rust
real 0m0.001s
user 0m0.001s
sys 0m0.000s
$ time wasmer run hello.wasm
Hello, Rust
real 0m0.014s
user 0m0.014s
sys 0m0.000s
1 msのRustネイティブコンパイルに比べると遅いですが、14msなので中々優秀ですね。最終的な実行速度ももちろん重要ですが、こういった軽快に起動するという要素も最近は強く求められてると思うのでとても良い傾向です。
ちなみに遅いと評判?のJDKですらOpenJDKでHello Worldを実行する場合は30msしかかからないので、読み込むモジュールが増えると速度差が出てくると思うので参考までに。
続いて速度をチェックしましょう。とはいえ本格的なベンチマークは面倒なのでフィボナッチ数列でいきます。以下のコードを書きます。
fn fib(n: u64) -> u64 {
return match n {
0 => 0,
1 => 1,
_ => fib(n - 2) + fib(n - 1)
}
}
fn main(){
let n = 42;
print!("Fib({})={}", n, fib(n));
}
まずはRustネイティブでビルドして実行します。
root@a939e23e2ef6:~# rustc fib.rs
root@a939e23e2ef6:~# time ./fib
Fib(42)=267914296
real 0m2.284s
続いて、WASM版は以下の通り。
rustc --target wasm32-wasi fib.rs
root@a939e23e2ef6:~# time wasmer run fib.wasm
Fib(42)=267914296
real 0m9.201s
user 0m9.270s
sys 0m0.051s
8倍以上遅くなりました。続いてLLVMによる最適化オプションを付けてコンパイルしてみます。
$ wasmer compile --llvm -o fib.wjit fib.wasm
$ time wasmer run fib.wjit
Fib(42)=267914296
real 0m4.415s
user 0m4.415s
sys 0m0.000s
2倍近く速くなりましたね! さらにnativeコンパイルもしてみます。
$ wasmer compile --native --llvm -o fib.so fib.wasm
$ time wasmer run ./fib.so
Fib(42)=267914296
real 0m4.424s
user 0m4.423s
sys 0m0.000s
特に速度差は出ていないのでnativeイメージにすればパフォーマンスが向上するというわけでもなさそうです。
性能サマリ
上記の結果とネイティブコンパイルした起動時間も含めてまとめてみました。
厳密に平均とかは取ってないですが一応複数回実行して大体同じ値になっています。
/ | Rustネイティブ | WASM | WASM(+最適化) | WASM(+ネイティブ) |
---|---|---|---|---|
起動速度 | 0.001s | 0.014s | 0.013s | 0.005s |
実行速度 | 2.284s | 9.201s | 4.415s | 4.424s |
Rust版のネイティブ実装に比べるとさすがに遅いですが、最適化オプションやネイティブイメージを利用すればかんり良い感じのパフォーマンスにはなりそうです。フィボナッチ数列なので参考程度の情報でしかないですが、少なくと遅すぎて使い物にならないとかは無さそう。
一方で、実行可能ファイルではないのでwasmerは現時点ではどちらにしろ要りそうですし、ネイティブコンパイルするかどうかはユースケースにかなり依存しそうです。ユニバーサルを捨てることになると思うのでその辺の棲み分けというか使い分けが重要になりそうです。
他言語からの呼び出し
今回は試してないですが、Wasmerの特徴の一つに他言語からの呼び出しがあります。
C/C++やRust, あるいはJavaScriptはもちろんですが、GoやPython、あるいはJavaやRubyといった様々な言語から呼び出すためのバインディングが提供されています。
他の言語に組込みやすいというのはJVMなどとは大きな特徴の違いだと思います。GraalVMのPolyglotも主従が逆だし。
そのため将来的には従来はsoやdllに任せていたような処理をwasmやwjitで書き、CPU, OS, 言語に捕らわれないユニバーサルなライブラリを作ることが出来る可能性があります。
Wasmerは何に使えそうなのか?
とりあえず動かしてみた分けですが、これ何に使えるんでしょうか? ちょっと考えてみます。
- アプリケーションの実行ランタイム
- ユニバーサルなライブラリ
- アプリケーション組み込みのマクロ
- FaaS/PaaS/マイクロサービス実行基盤
だいたい、このあたりかな? と思います。
アプリケーションの実行ランタイム
これはelectonやあるいはJVM/.NETがそうであるようにアプリケーションの実行ランタイムとしての利用です。JVMと比べても速度はさておきフットプリントは軽そうに見えるのでWASMを出力できる処理系が増えれば用途は広がるかもしれません。IoTとかでも動かせると良いですね。目指せ30億のデバイス!
ユニバーサルなライブラリ
他の言語から呼び出すことができるのでユニバーサルなライブラリとしての活用が期待できます。ここでいうユニバーサルとはCPU/OSに依存せずに利用できるライブラリという意味ですね。
従来は.dllとか.soとか同じコードであってもABI互換は無いので再コンパイルする必要がありましたが、これが不要になるのであれば用途は広がるかもしれないです。
ただ、現時点ではネイティブコンパイルをする事で起動時間を短縮できますが、そうしてしまうとユニバーサルじゃなくなるので、この辺のエコシステムや運用ノウハウがどう組みあがっていくかが今後の課題な気がします。
アプリケーション組み込みのマクロ
Wasmerはアプリケーションにも組込めますのでマクロ(ミニ言語/DSL)としての利用が期待できます。
実際、サービスメッシュのLinkerd2-proxyは実装がWasmerか分かりませんがWASMをプラグインとして利用できます。
RustやC/C++はもちろんWASMは様々な言語で今後対応される見込みなのでプラグインの基盤等にはかなり向いているんじゃないかと思います。
今後、この用途は増えていきそうな予感がします。
FaaS/PaaS/マイクロサービス実行基盤
これは端的に言ってしまえばDockerコンテナの代わりにWasmerを使うというものです。そのため、Wasmerをコンテナとして紹介してる記事もあります。
DockerやRuncに代表されるLinuxコンテナは非常に便利でLinuxで出来ることが何でもできるからこそ、ここまで普及したというのはあると思います。既存コマンド動かしたりいろんな無茶が出来ますしね。
しかしながら、普段から常にLinuxのフルスペックが必要かとそうでもなく、FaaS/PaaSのレイヤで見ればアプリケーションさえ実行されてくれれば良いわけです。
その目的によりコンパクトでフットプリントが小さくセキュアなWASMを使おうという流れがあり、Wasmerもそこにハマる実装だと思います。
実際マイクロソフトの開発しているKubelet等はk8s上でWASMを実行する事が出来るので、将来的にはこういったLinuxコンテナよりも軽量なランタイムがk8s上で基本的には動かされ、複雑な処理をするときにはLinuxコンテナを使うという感じで棲み分けがk8sの上でされていく気がします。
まとめ
前からWASMをブラウザの外で実行するというのは気になってたので、Wasmerが1.0になったということで触ってみました。
割と軽量に動きそうなのでユニバーサルなプラットフォームとしてJavaが見た夢を果たせると面白いですね。2倍程度の性能差ならネイティブじゃなくても十分使い道がありそうですし。個人的にはFaaS/PaaSのプラットフォームやマクロ/プラグイン基盤として注目してます。
ちなみに今回は触ってませんが、CやRustといった他言語の変換だけではなく、S式を使ってWASMを直接書くこともできるので機会があればそれも触ってみたいです。
それではHappy Hacking!
Discussion