⏱️

Wasmer 1.0がリリースされたので、ベンチマークを取ってみた

2021/02/02に公開

先日、WebAssemblyランタイム Wasmer (https://wasmer.io/) のバージョン1.0がリリースされました。

https://twitter.com/wasmerio/status/1346528553676881920

WebAssemblyは元々はウェブブラウザで実行するための低水準のバイナリコードフォーマットとして導入されましたが、単にポータブルな仮想マシンと見ることもできるので、これを用いてアプリケーション実行環境を作ろうというのも、ごく自然な流れだと言えるでしょう。

WebAssembly公式によって WASI (https://wasi.dev/) というシステムインターフェースが定義されています。まだネットワークAPIが無かったりして、多くのアプリケーションが動かせるという状況ではなさそうですが、将来的にポータブルなアプリケーション実行環境として使えるようになるかもしれません。こう聞くと、誰もがJavaを想起すると思いますが、特定の会社の意向に左右されないオープンな代替物ができるというのは、めぐりめぐって辿り着いた数十年前のコンセプトだったとしても、それはそれで意味があることだとは思います。

そういった用途として安心して使えるには、機能だけではなく、充分なパフォーマンスを備えていることが必要でしょう。今回バージョン1.0がリリースされたということなので、現時点でどの程度の性能があるのかベンチマークを取ってみました。

https://medium.com/wasmer/wasmer-1-0-3f86ca18c043

ここにもパフォーマンスに関する言及が少しあって、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 の三つのバックエンドがあります。他に jitnative のオプションもあるみたいですが、ベンチいマーク結果が cranelift と全く同じだったので、エイリアスではないかと思います。なので、この三つのバックエンドそれぞれに対してベンチマークを取りました。

実行時間の計測は hyperfine (https://github.com/sharkdp/hyperfine) を用いて、ウォームアップありで最低実行回数10回で統計を取りました。ウォームアップによって、JVMやWasmerのコンパイルがキャッシュされているものと期待しています。ベンチマークからコンパイル時間は除外されるようにしていますが、今回のベンチマークに用いた100KB程度のバイナリなら、体感的にはいずれのバックエンドでも起動時間は気にならない程度であったと付け加えておきます。

結果

ベンチマーク結果のグラフを次に示します。縦軸は rust-native を 1 とした実行時間の相対値です。短い方が速いです。

ちょっと酷いグラフになっていますね。singlepass バックエンドの挙動がちょっと怪しくて、nbodymandelbrotなど、ヘビーに浮動小数点演算を行うコードがものすごく遅くなっています。遅いだけじゃなくて、試行ごとの実行時間も安定しなくて、平均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