Wasmer 1.0がリリースされたので、ベンチマークを取ってみた
先日、WebAssemblyランタイム Wasmer (https://wasmer.io/) のバージョン1.0がリリースされました。
WebAssemblyは元々はウェブブラウザで実行するための低水準のバイナリコードフォーマットとして導入されましたが、単にポータブルな仮想マシンと見ることもできるので、これを用いてアプリケーション実行環境を作ろうというのも、ごく自然な流れだと言えるでしょう。
WebAssembly公式によって WASI (https://wasi.dev/) というシステムインターフェースが定義されています。まだネットワークAPIが無かったりして、多くのアプリケーションが動かせるという状況ではなさそうですが、将来的にポータブルなアプリケーション実行環境として使えるようになるかもしれません。こう聞くと、誰もがJavaを想起すると思いますが、特定の会社の意向に左右されないオープンな代替物ができるというのは、めぐりめぐって辿り着いた数十年前のコンセプトだったとしても、それはそれで意味があることだとは思います。
そういった用途として安心して使えるには、機能だけではなく、充分なパフォーマンスを備えていることが必要でしょう。今回バージョン1.0がリリースされたということなので、現時点でどの程度の性能があるのかベンチマークを取ってみました。
ここにもパフォーマンスに関する言及が少しあって、clangをwasmにコンパイルしたclang.wasm
のコンパイル時間が、Wasmer 0.17.1からWasmer 1.0で最大で9倍速くなったなどとあります。ただ、コンパイルしたコードの実行時間に関してはよくわからなかったので、コードを用意して性能を図ってみました。
ベンチマーク
ベンチマークには、色々なアルゴリズムを色々なプログラミング言語で実装して速度を計測している The Computer Language Benchmarks Game のコードを利用しました。WasmのバイナリはRustのコードからコンパイルしたものを用いました。さらに、比較対象としてRustを直接ネイティブにコンパイルしたもの、速いプログラミング言語の代表としてC言語、競合になりそうなJava、さらに別の WebAssembly+WASI 実行環境としてWasmtime (https://wasmtime.dev/) での実行時間を計測しました。
なお、pidigits (円周率の計算) は GMP を呼ぶコードになっていたためwasmにコンパイルできず、regex-redux は同様にpcreを使っているためにコンパイルできなかったので、それぞれベンチマークからは除外しています。また、wasmのマルチスレッドは現在策定段階で、Rustからのコンパイルでは動かなかったので、すべて1スレッドのみを用いるようにして計測しています。計測に用いたコードは https://github.com/tanakh/wasmer-bench に置いてあります。
wasmerには、コンパイル速度が重要なアプリのための singlepass
、実行速度が重要なアプリのための llvm
、中間的な性能の(?)cranelift
の三つのバックエンドがあります。他に jit
、native
のオプションもあるみたいですが、ベンチいマーク結果が cranelift
と全く同じだったので、エイリアスではないかと思います。なので、この三つのバックエンドそれぞれに対してベンチマークを取りました。
実行時間の計測は hyperfine (https://github.com/sharkdp/hyperfine) を用いて、ウォームアップありで最低実行回数10回で統計を取りました。ウォームアップによって、JVMやWasmerのコンパイルがキャッシュされているものと期待しています。ベンチマークからコンパイル時間は除外されるようにしていますが、今回のベンチマークに用いた100KB程度のバイナリなら、体感的にはいずれのバックエンドでも起動時間は気にならない程度であったと付け加えておきます。
結果
ベンチマーク結果のグラフを次に示します。縦軸は rust-native
を 1 とした実行時間の相対値です。短い方が速いです。
ちょっと酷いグラフになっていますね。singlepass
バックエンドの挙動がちょっと怪しくて、nbody
やmandelbrot
など、ヘビーに浮動小数点演算を行うコードがものすごく遅くなっています。遅いだけじゃなくて、試行ごとの実行時間も安定しなくて、平均928秒の標準偏差592、最小49.8秒、最大1500秒というちょっとわけのわからない数字になりました。どういう理屈でこんなことになっているのか気になるところではあるので、このあたり詳しく原因を調べてみたいところです。
とりあえずこれではグラフが何も見えないので、著しく遅い行を省いたものが次のグラフです。
WebAssemblyランタイムの中では、Wasmerのllvm
バックエンドが安定して一番速い感じで、cranelift
バックエンドとWasmtmeは一段階速度としては落ちるような感じでしょうか。singlepass
バックエンドは速度よりも性能の不安定さが不安な気がします。Wasmtimeもrevcomp
が妙に遅いですが、試行ごとの実行時間は安定していました。revcomp
は1GB近いテキストファイルを読むIOヘビーなタスクなので、もしかしたらWasmtimeはそこら辺の性能が低いのかもしれません。
さらにわかりやすくするために、WebAssemblyのランタイムを最速のwasmer-llvm
のみにしたグラフを次に示します。
これを見る限り、Wasmerのllvm
バックエンドはかなり良好なパフォーマンスを示していると言えるでしょう。ネイティブコードと比較しても、ワーストがmandelbrot
の2倍程度で、fasta
のように、なぜかネイティブより大幅に速いものもありました。C言語と比較しても遜色なく、Javaとの比較だとWasmerのほうが優位とも言えそうです。
最後に、正しいベンチマーク結果の要約の仕方 に基づいて、処理系ごとのGeometric meanのグラフを示しておきます。
singlepass
が突出していますが、極端に遅いものがあるので、そういうものを除けばcranelift
やWasmtimeとそこまで差がないかもしれません。CやRustのネイティブコードと比べると、Wasmerのllvm
バックエンドは若干遅いといったレベルで、テスト間で極端に遅いといったものもなく、かなり優秀な結果といえると思います。特に、すでにJavaに比べて速度面で若干上回っているのは、ポータブルなバイナリとしてwasmを使うことの、速度面での不安を払拭できるのではないでしょうか。
個人的にも、正直思ったより性能が良かったです。あとはネットワークのサポートが追加されて、スレッドサポートが正式版なったら、もっと広範なアプリケーションに積極的に使っていけそうで、今後が楽しみです。
Discussion