🤖

Wasm と C++ と JavaScript の詳細なベンチマークを作った

に公開1

Wasm と C++ と JavaScript の詳細なベンチマークを作りました。Wasm のベンチマークはいくつかあって 以前も様々な言語ごとのベンチマーク を作ったことがあるのですが、Wasm の最高性能を測定するベンチマークは今のところないように思います。Wasm で最高性能を出すためには SIMD とスレッドを考慮しなければいけませんが、それらをきちんと考慮したベンチマークを作るのは難しいからだと思います。

そのため、もともと SIMD とスレッドを考慮したライブラリを使ってベンチマークを作るのが良いです。さらに C++/Rust で作られたライブラリであることが望ましいです。理由は Wasm ビルド以外にも asm.js ビルドに対応しているため、C++/Rust コードを自動で JavaScript に変換することができ、比較がしやすいからです。

SIMD やスレッドをきちんと考慮しているライブラリは実はそれほど多くはなく、画像処理なら OpenCV、動画処理なら ffmpeg、深層学習なら Tensorflow、数値計算なら Eigen などを利用することになるでしょう。この中で様々なタスクを調査しやすいのは OpenCV のため、OpenCV を Wasm 化した opencv.js のベンチマークを作って公開しました。

https://github.com/marmooo/opencvjs-bench

このベンチマークでは OpenCV でよく使う関数を C++ をネイティブビルドしたものと、Wasm、JavaScript に変換したものの実行時間を計測します。ビルド時の最適化オプションによる性能差を C++/Wasm で比較するベンチマークと、SIMD とスレッドの考慮による性能差を C++/Wasm/JavaScript で比較するベンチマークを用意しました。

Wasm は初回実行に多少時間が掛かるので、初回実行時の速度と、平均速度の 2つを計測します。平均速度は初回実行後に 3回の warmup を行い、5回の平均を取ったものです。不満があれば設定できるようにしてあります。使い方は以下。インストールもビルドもベンチマークも自動化されており、実行結果は Markdown できれいに表示されます。

git submodule update --init --recursive
bash build_emsdk.sh
bash build_opencv.sh
bash build_cpp.sh
bash build_js.sh
bash build_wasm.sh
deno task wasm_type_bench
deno task wasm_opt_bench
deno task cpp_opt_bench

ビルドに凄まじく時間が掛かるので、ベンチマーク結果も載せましょう。計測環境はこちら。

    CPU | Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz
Runtime | Deno 2.3.3 (x86_64-unknown-linux-gnu)

まずは Wasm の種類ごとの比較 (wasm_type_bench) がこちら。JavaScript と Wasm の速度は大差がなく、平凡な Wasm はタスクをうまく選んだ上で JavaScript の 1.0〜1.8倍速くらいが良いところです。十分な難易度とループがあって初めて多少の速度差が出るくらいで、それほど高速とは言えません。タスク別に見ると JavaScript は stackBlur が妙に遅いですが、理由は不明です。Wasm は SIMD の効果が大きく、SIMD + Threads を最大限に活かして初めて高速と言えます。とはいえ C++ ほどは速度が出ません。稀に Wasm + SIMD + Threads が C++ に勝てていますが、C++ の 1〜1/8 くらいが実力値でしょう。現時点の最高性能である Wasm + SIMD + Threads は JavaScript の 2〜10倍速となりました。C++ は JavaScript の 2〜90倍速となりました。当たり前ですが、やはり C++ は強い。

firstRun

task js wasm simd threads threaded-simd cpp
split 373.353 278.429 106.417 279.395 94.854 53.935
LUT 157.598 305.115 310.117 138.011 143.475 28.545
adaptiveThreshold 257.386 264.099 191.054 294.341 204.199 62.726
blur 628.278 494.055 233.620 586.787 266.381 65.231
Canny 1171.524 855.231 345.041 658.339 198.531 125.123
cvtColor 93.521 67.932 58.501 28.490 29.248 13.090
boxFilter 610.106 493.666 232.115 598.711 267.909 65.037
dilate 912.217 446.435 54.292 479.961 59.739 14.493
erode 911.660 564.615 54.411 513.298 61.958 14.512
findContours 217.194 160.295 60.420 149.942 76.075 50.105
GaussianBlur 5415.832 4638.947 991.653 3107.302 504.754 128.199
resize 188.441 265.993 123.520 113.411 56.828 11.786
stackBlur 3503.998 3916.726 2225.072 2665.952 1223.825 66.621

avg

task js wasm simd threads threaded-simd cpp
split 105.055 96.008 23.009 116.461 22.993 53.889
LUT 150.107 117.206 120.005 65.506 65.306 28.684
adaptiveThreshold 207.996 165.689 128.371 200.478 139.566 63.383
blur 609.761 492.094 232.468 588.287 261.690 65.300
Canny 676.654 505.775 210.705 291.552 116.902 100.587
cvtColor 73.690 64.109 57.686 28.508 28.120 13.119
boxFilter 612.667 494.609 234.279 588.501 259.219 65.818
dilate 796.754 459.149 48.135 481.591 56.465 14.553
erode 774.569 426.891 48.191 487.951 57.103 14.584
findContours 101.017 89.131 40.083 77.289 45.244 47.351
GaussianBlur 5370.792 3975.270 975.118 2952.571 491.730 128.137
resize 161.129 136.820 70.161 77.643 36.049 12.234
stackBlur 5619.222 1537.115 913.361 1251.302 582.953 65.224

C++ の最適化オプションごとの比較 (cpp_opt_bench) がこちら。やや処理が簡単すぎることもあって、あまり安定しない結果になっています。GaussianBlur や resize のように最適化が効きやすい処理だと -O2 や -O3 で大幅に高速化される可能性がある感じでしょうか。

firstRun

task -O1 -O2 -O3 -Ofast -Os
split 99.755 88.586 76.602 121.259 77.191
LUT 60.166 84.464 71.387 59.272 56.196
adaptiveThreshold 247.097 231.559 250.999 213.653 251.840
blur 66.763 66.531 65.157 65.209 66.671
Canny 205.637 146.256 203.226 162.714 172.969
cvtColor 14.929 15.069 22.012 15.163 15.159
boxFilter 87.399 67.041 65.073 65.342 65.322
dilate 25.780 26.387 25.616 21.346 18.290
erode 14.985 14.927 14.921 14.808 14.988
findContours 58.543 69.890 64.452 64.093 69.932
GaussianBlur 445.044 210.282 247.940 243.811 233.270
resize 151.833 137.772 45.517 37.849 43.858
stackBlur 65.337 65.175 64.949 65.100 66.434

avg

task -O1 -O2 -O3 -Ofast -Os
split 54.666 54.753 55.568 55.178 54.909
LUT 28.841 28.842 28.883 28.823 28.887
adaptiveThreshold 62.694 63.270 63.901 63.859 64.335
blur 65.336 65.663 65.387 65.629 66.321
Canny 102.486 105.585 104.163 103.027 103.333
cvtColor 15.068 17.733 16.333 15.087 15.111
boxFilter 65.813 65.522 65.357 65.461 66.102
dilate 14.811 14.878 14.827 14.845 14.903
erode 14.817 14.871 14.840 14.958 14.868
findContours 44.996 44.462 44.980 44.967 48.306
GaussianBlur 129.042 140.020 128.698 128.722 128.526
resize 14.184 14.065 14.167 14.124 14.192
stackBlur 65.500 67.943 65.497 66.199 66.156

Wasm の最適化オプションごとの比較 (wasm_opt_bench) がこちら。C++ の GaussianBlur の最適化オプションの効果がなくなってます。-O1 よりは -O2 のほうが早い印象はありますが、-O2 と -O3, -Ofast の差は stackBlur でしか出ませんでした。出力サイズを小さくしたい場合、-Oz は -O1 くらいしか速度が出ないので、-Os を使ったほうが良いように思います。

firstRun

task -O1 -O2 -O3 -Ofast -Os -Oz
split 96.849 94.670 95.449 96.858 94.913 97.114
LUT 160.330 133.418 134.614 133.017 159.429 157.065
adaptiveThreshold 198.377 210.466 201.939 217.420 202.389 243.391
blur 312.803 266.481 268.708 263.816 265.048 321.619
Canny 405.961 186.103 213.979 208.886 256.203 265.221
cvtColor 26.652 26.456 28.848 28.359 28.507 39.551
boxFilter 303.032 270.001 261.692 261.724 268.009 326.101
dilate 59.965 60.145 62.845 59.825 56.321 56.834
erode 56.412 60.726 62.736 58.750 56.037 57.245
findContours 74.230 72.971 77.849 80.289 98.590 83.920
GaussianBlur 427.159 432.342 504.440 594.490 436.824 877.868
resize 40.041 56.608 57.625 58.978 55.290 82.066
stackBlur 1209.108 1183.963 1044.843 1076.938 1212.346 1252.409

avg

task -O1 -O2 -O3 -Ofast -Os -Oz
split 23.162 22.898 22.921 22.803 22.923 30.622
LUT 87.041 65.465 67.993 65.309 87.082 82.496
adaptiveThreshold 137.433 136.109 142.515 138.929 146.078 159.763
blur 303.932 262.939 262.555 261.769 261.677 326.615
Canny 130.343 106.557 117.154 115.361 146.332 164.536
cvtColor 26.563 26.352 28.110 28.196 26.379 39.508
boxFilter 303.363 262.055 264.933 266.775 264.476 324.007
dilate 54.910 55.352 55.772 56.473 54.019 54.251
erode 54.092 55.160 57.617 57.681 54.217 55.048
findContours 54.302 47.940 44.868 45.503 53.265 53.147
GaussianBlur 419.123 419.395 492.748 494.798 422.994 871.773
resize 32.110 35.904 35.964 36.661 37.482 66.304
stackBlur 559.025 616.755 584.184 588.932 610.183 725.235

結論としては、Wasm + SIMD + Threads はネイティブビルドには勝てませんが、まあまあいい勝負ができるといったところです。-O2 以上の最適化はそれほど効いていないように見えますが、もう少し複雑な処理にしないと差がわかりにくいかも知れません。それでも最高性能が得られているのはやはり -O3 以上なので、現状ではデフォルトの -O3 がちょうど良いのではないかと思います。Wasm は V8 などの処理系による高速化には期待できますが、現状ではネイティブビルドほどは性能が出ないので、動画のエンコードや深層学習などの処理速度はまだ課題かも知れません。それらについては誰かがそのうちベンチマークを作ってくれることに期待しましょう。肌感としては画像処理くらいなら処理時間も少ないし使いやすいかなという印象で、画像処理アプリをいくつか作ってみました。これくらいのアプリだと高速感を感じられるので良い感じです。

https://marmooo.github.io/lineart-converter/
https://marmooo.github.io/cv-npr/

Discussion

togetoge

興味深い検証ありがとうございます。コードも含めて勉強になります。
gccのコンパイル時に-march=skylakeを合わせて付与するとAVX2あたりのSIMD命令を使うようになって高速化するかもしれません。
Core i5 6200なのでそこまで顕著に差がでないかもしれませんが。

ご参考までに。