Wasm と C++ と JavaScript の詳細なベンチマークを作った
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 のベンチマークを作って公開しました。
このベンチマークでは 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 などの処理系による高速化には期待できますが、現状ではネイティブビルドほどは性能が出ないので、動画のエンコードや深層学習などの処理速度はまだ課題かも知れません。それらについては誰かがそのうちベンチマークを作ってくれることに期待しましょう。肌感としては画像処理くらいなら処理時間も少ないし使いやすいかなという印象で、画像処理アプリをいくつか作ってみました。これくらいのアプリだと高速感を感じられるので良い感じです。
Discussion
興味深い検証ありがとうございます。コードも含めて勉強になります。
gccのコンパイル時に
-march=skylake
を合わせて付与するとAVX2あたりのSIMD命令を使うようになって高速化するかもしれません。Core i5 6200なのでそこまで顕著に差がでないかもしれませんが。
ご参考までに。