🏎️

【WebAssembly】Wasmer で計算プログラムを実行するベンチマークをしてみた

7 min read

Wasmer という WebAssembly (wasm) ランタイムが話題となり、wasm はブラウザ以外からも実行可能なバイナリフォーマットであるという認識が広まりつつあります。

Wasmer 上での動作がどの程度速いものか気になったので、ある計算プログラムを wasm にコンパイルしたものと macOS 向けにコンパイルしたものを用意してベンチマークしてみることにしました。

検証に用いる計算プログラム

グラフ理論的な計算プログラムである nauty-geng が C 言語で記述されているので、Emscripten で wasm にコンパイルして用いることにしました。

nauty-geng では与えられた条件を満たす同型ではない無向グラフを列挙したり、数え上げることができます。

頂点の数が 9 である木の列挙(47 個)

動作確認環境

  • MacBook Pro (16-inch, 2019)
    • Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
    • メモリ 16GB
    • macOS Catalina
  • Wasmer 1.0.0
  • Emscripten 2.0.12
  • nauty and Traces 2.7r1

事前準備

Wamer の入手とインストール

curl https://get.wasmer.io -sSfL | sh

https://docs.wasmer.io/ecosystem/wasmer/getting-started

Emscripten の入手とインストール

https://emscripten.org/docs/getting_started/downloads.html

nauty and Traces の入手と nauty-geng のコンパイル

nauty and Traces のサイトからソースコード(gzipped tar file)をダウンロードします:

curl -O https://pallini.di.uniroma1.it/nauty27r1.tar.gz
tar zxvf nauty27r1.tar.gz
cd nauty27r1/

makefile のターゲット geng に倣い、emcc コマンドで nauty-geng を geng.wasm としてコンパイルします:
(オプションに -DNAUTY_CPU_DEFINED -DCPUTIME=0.0 を指定する必要がありました[1])

$ emcc -o geng.wasm -O3 -DMAXN=WORDSIZE -DWORDSIZE=32 -DNAUTY_CPU_DEFINED -DCPUTIME=0.0 geng.c gtools.c nauty.c nautil.c naugraph.c schreier.c naurng.c
$ ls -l geng.wasm
-rwxr-xr-x  1 mascii  staff  96147  1 15 22:20 geng.wasm*

比較のために makefile を用いて gcc でもコンパイルしておきます:

$ make geng
$ file ./geng
./geng: Mach-O 64-bit executable x86_64
$ ls -l ./geng
-rwxr-xr-x  1 mascii  staff  155412  1 15 22:20 ./geng*

コンパイルしたファイルの動作確認

geng.wasm に渡すオプションは -- 以降に記述します。

$ wasmer geng.wasm -- -uc 9 8
>A geng.wasm -cd1D8 n=9 e=8
>Z 47 graphs generated in 0.00 sec
$ ./geng -uc 9 8
>A ./geng -cd1D8 n=9 e=8
>Z 47 graphs generated in 0.00 sec

wasm にコンパイルしたものと macOS 向けにコンパイルしたものが同じように動作していることを確認できました。

なお、geng に渡した各オプションの意味は以下の通りです:

  • -u: 数え上げのみを行う[2]
  • -c: 連結グラフを対象とする
  • 9 8: 頂点の数: 9, 辺の数: 8 であるグラフを対象とする

頂点の数が n, 辺の数が n - 1 であるような連結グラフはとなることが知られています。木の列挙や数え上げは geng よりも gentreeg の方が高速ですが、本記事では geng のみを取り扱います。

ベンチマークしてみた

geng に渡すオプションは以下のものとし、それぞれ 10 回実行して実行時間を測定します。

  1. -uc 20 19 (頂点の数が 20 の木の数え上げ)
  2. -ucd2D4 13 (頂点の数が 13 の最小次数: 2, 最大次数: 4 である連結グラフの数え上げ)
  3. -ucd3D3 20 (頂点の数が 20 の連結な立方体グラフの数え上げ)

1. -uc 20 19

./geng -uc 20 19
wasmer geng.wasm -- -uc 20 19
wasmer run --llvm geng.wasm -- -uc 20 19
1 2 3 4 5 6 7 8 9 10 avg.
native 11.06s 11.10s 10.97s 11.10s 11.05s 10.96s 11.01s 10.97s 10.96s 11.05s 11.02s
wasmer 25.78s 25.82s 25.81s 25.85s 25.78s 25.85s 25.94s 25.69s 25.98s 25.80s 25.83s
wasmer (llvm) 12.71s 12.52s 12.59s 12.54s 12.67s 12.52s 12.62s 12.53s 12.57s 12.72s 12.60s

(数え上げられたグラフの数: 823065)

LLVM による最適化を行うと、ネイティブとかなり近い速さになっていることがわかりました。
次のオプションからは LLVM による最適化を行った結果のみを測定します。

2. -ucd2D4 13

./geng -ucd2D4 13
wasmer run --llvm geng.wasm -- -ucd2D4 13
1 2 3 4 5 6 7 8 9 10 avg.
native 57.78 57.64 57.24 57.39 57.24 57.46 57.44 57.39 57.38 57.39 57.44s
wasmer (llvm) 72.7 72.44 72.29 72.52 72.49 72.94 72.68 72.61 72.8 72.51 72.60s

(数え上げられたグラフの数: 34734530)

wasmer はネイティブに比べ 1.25 倍程度の時間がかかりました。実行時間のばらつきは少ないようでした。

3. -ucd3D3 20

./geng -ucd3D3 20
wasmer run --llvm geng.wasm -- -ucd3D3 20
1 2 3 4 5 6 7 8 9 10 avg.
native 219.43 219.5 216.98 217.65 216.24 215.27 216.6 217.8 218.65 218.81 217.69s
wasmer (llvm) 274.37 273.34 272.8 273.53 273.58 274.35 273.9 274.42 274.33 273.54 273.82s

(数え上げられたグラフの数: 510489)

より計算に時間がかかるこのオプションでも wasmer はネイティブに比べ 1.25 倍程度の時間がかかりました。

ポータビリティも確かめてみた

実行時間だけでなく、実行環境に依存しないポータビリティについても確かめてみました。

Raspberry Pi

Raspberry Pi は ARM プロセッサを搭載していて、Raspberry Pi OS という Debian 系 Linux がよく用いられています。

Raspberry Pi 3 Model B に Raspberry Pi OS Lite をインストール後、Running Wasmer on a Raspberry Pi 4 with 64-bit userland を見て wasmer をインストールし、scp で wasm ファイルを転送して実行してみました:

(pi64)pi@raspberrypi:~ $ time wasmer geng.wasm -- -uc 20 19
>A geng.wasm -cd1D19 n=20 e=19
>Z 823065 graphs generated in 0.00 sec

real    1m23.641s
user    1m23.579s
sys     0m0.057s

LLVM による最適化を行おうとしましたが、エラーが出てしまいました:

(pi64)pi@raspberrypi:~ $ wasmer run --llvm geng.wasm -- -ucd2D4 13
error: failed to run `geng.wasm`
│   1: module instantiation failed (engine: jit, compiler: llvm)
╰─> 2: Compilation error: unknown ELF relocation 283

Chrome

WebAssembly.sh という Web ターミナルに wasm ファイルをドラッグ&ドロップするだけで、WASI モジュールを実行できました。
WebAssembly.sh で geng.wasm を実行している様子

まとめ

wasm はポータビリティを持ち合わせながらも、Wasmer で最適化して実行することで大きな性能の落ち込みがないことを確認できました。
これに加え、サンドボックス化された実行環境上で実行されることも wasm を利用するメリットなので、今後のエコシステムの発展が楽しみです。

脚注
  1. (計算後のCPU時間) - (計算前のCPU時間) を表示する機能を無効化します。こうすることで wasmer での実行時のエラー(Error while importing "env"."times": unknown import. Expected Function(FunctionType { params: [I32], results: [I32] }))を回避できました ↩︎

  2. -u オプションを外すと Graph6 というテキスト形式でグラフが列挙されます ↩︎