WebAssembly SIMD を pixelmatch に適用した
ついに Safari にWebAssembly SIMD
が来るとのことで試してみた。
対象はpixelmatch
あたりが手頃そうなのでこれを対象とする。
Safari as the last browser finally supports WebAssembly SIMD 🎉 pic.twitter.com/HVX3gZTrRi
— CryZe (@CryZe107) December 23, 2022
目次
モチベーション
Rust
からWebAssembly SIMD
をどう使うのか学びたいWebAssembly SIMD
によるパフォーマンスの変化を観測したい
またreg-viz/reg-cli
がpixelmatch
に依存しており、reg-viz/reg-cli
をRust
,wasm
製にしたいという活動の一環でもある。
成果物
https://github.com/bokuweb/pixelmatch-rs/pixelmatch-simd-wasm
前提
x86
と記載されているものはMacBook Pro (13-inch, 2019) 2.8 GHz クアッドコアIntel Core i7
を使用したものとするarm
と記載されているものはMacBook Air(M2、2022)Apple M2
を使用したものとするNode.js
はv19.4.0
を使用したものとするChrome
はv109
を、Firefox
は110
をSafari
はTechnologyPreview
の16.4
を使用したものとする
pixelmatch とは
pixelmatch
はピクセルレベルで画像の差分を取るライブラリでオリジナルはmapbox/pixelmatch、今回はこれをRust
に porting したものを対象とする。
Example Outputを見ると、どういうものかは分かりやすい。
SIMD への変更
どのような変更となるか一例をあげておく。
fn rgb2y(r: f32, g: f32, b: f32) -> f32 {
r * 0.29889531 + g * 0.58662247 + b * 0.11448223
}
たとえば上記のような任意のピクセルの各色に定数を乗算する既存関数は以下のように書き換えることができる。
const V_Y: v128 = f32x4(0.29889531, 0.58662247, 0.11448223, 1.0);
fn rgb2y(px: v128) -> f32 {
let v = f32x4_mul(px, V_Y);
sum(v)
}
ただし、事前にv128
に変換する必要があり今回は以下のように前段で詰替えを行っている。
let p = f32x4(
img1[pos] as f32,
img1[pos + 1] as f32,
img1[pos + 2] as f32,
img1[pos + 3] as f32,
);
この要領でv128
を使えそうな箇所を書き換えていった。
機械語の確認
書き換えたはいいが、SSE/AVX/NEON
がちゃんと使用されているか見ておく必要がある。
命令の詳細はわからないが、今回はひとまずそれらが使用されていそうか確認した。
確認は以下で行える。(最初はd8
を使用していたが、node
のほうが楽なことに気づきこちらを使用している)
node --print-wasm-code --experimental-wasm-simd --no-liftoff simd.js
またhelp
には見当たらなかった(気がした)が--no-liftoff
をつけることによりturbofan
での結果を出力するようにしている。これがないとcompiler
がliftoff
になってしまう。
x86
前述したrgb2y
相当な箇所を探してみたら以下を見つけた。3f162ce4
が0.58662247
に相当、3e9908ce
が0.29889531
に相当するのでvmovq xmm1,r10
, vpinsrq xmm1,xmm1,r10,0x1
などでxmm1
にV_Y
を移し、vmulps xmm0,xmm0,xmm1
で乗算をしているように見える。
調べてみるとfloat32 ベクトルを乗算します。対応するインテル® AVX 命令は VMULPS です。
とあるので確かにAVX
命令が使用されていそうなことが確認できた。
0x30f2b684e0cc ac c4e37921c110 vinsertps xmm0,xmm0,xmm1,0x10
0x30f2b684e0d2 b2 49bace08993ee42c163f REX.W movq r10,0x3f162ce43e9908ce
0x30f2b684e0dc bc c4c1f96eca vmovq xmm1,r10
0x30f2b684e0e1 c1 49baa975ea3d00000000 REX.W movq r10,0x3dea75a9
0x30f2b684e0eb cb c4c3f122ca01 vpinsrq xmm1,xmm1,r10,0x1
0x30f2b684e0f1 d1 c4e37921c220 vinsertps xmm0,xmm0,xmm2,0x20
0x30f2b684e0f7 d7 c5f859c1 vmulps xmm0,xmm0,xmm1
0x30f2b684e0fb db c5fa16c8 vmovshdup xmm1,xmm0
0x30f2b684e0ff df c5f828d0 vmovaps xmm2,xmm0
0x30f2b684e103 e3 c5f258ca vaddss xmm1,xmm1,xmm2
0x30f2b684e107 e7 c5f812c0 vmovhlps xmm0,xmm0,xmm0
0x30f2b684e10b eb c5f258c0 vaddss xmm0,xmm1,xmm0
0x30f2b684e10f ef 41bacdcccc3d movl r10,0x3dcccccd
arm
該当しそうなのは以下だろうか。3e9908ce
らしきものがあるし、その後fmul v0.4s, v0.4s, v1.4s
されているので間違ってはいなさそうだ。
vmul
のような命令が出てくるのかと想像していたが、どうもこれによるとprefix
は削除されたらしくオペランドがv0.4s, v0.4s, v1.4s
ことからもNEON
が使用されているようなことが確認できた。
0x1d92955dd25c 9c d28119d0 movz x16, #0x8ce
0x1d92955dd260 a0 f2a7d330 movk x16, #0x3e99, lsl #16
0x1d92955dd264 a4 f2c59c90 movk x16, #0x2ce4, lsl #32
0x1d92955dd268 a8 f2e7e2d0 movk x16, #0x3f16, lsl #48
0x1d92955dd26c ac 4e080e01 dup v1.2d, x16
0x1d92955dd270 b0 d28eb530 movz x16, #0x75a9
0x1d92955dd274 b4 f2a7bd50 movk x16, #0x3dea, lsl #16
0x1d92955dd278 b8 4e181e01 mov v1.d[1], x16
0x1d92955dd27c bc 6e140440 mov v0.s[2], v2.s[0]
0x1d92955dd280 c0 6e21dc00 fmul v0.4s, v0.4s, v1.4s
0x1d92955dd284 c4 5e0c0401 mov s1, v0.s[1]
0x1d92955dd288 c8 5e040402 mov s2, v0.s[0]
0x1d92955dd28c cc 1e212841 fadd s1, s2, s1
0x1d92955dd290 d0 5e140400 mov s0, v0.s[2]
0x1d92955dd294 d4 1e212800 fadd s0, s0, s1
ベンチマーク
以下にはbanchmarkjs
を使用して取得した平均値を記載している。
差分生成にかかった時間なので小さいほど高速だといえる。
画像は以下のようなケースを用意した。(diff
はimg1
,img2
をpixelmatch
にかけた場合に得られる結果)
img1 | img2 | diff | |
---|---|---|---|
ケース 1 800x578 | |||
ケース 2 7488x5242 | |||
ケース 3 10000x8000 |
ケース 1
x86
JavaScript | WebAssembly | WebAssembly SIMD | |
---|---|---|---|
Node.js | 0.008110802s | 0.008282832s | 0.01121049s |
Chrome | 0.008855555s | 0.014030864s | 0.01607272s |
Firefox | 0.024034090s | 0.010297727s | 0.01341049s |
arm
JavaScript | WebAssembly | WebAssembly SIMD | |
---|---|---|---|
Node.js | 0.004776049s | 0.003541175s | 0.004477764s |
Chrome | 0.004959325s | 0.003663859s | 0.004669467s |
Firefox | 0.016202702s | 0.004506140s | 0.005525669s |
Safari | 0.043800925s | 0.003248253s | 0.004444736s |
ケース 2
x86
JavaScript | WebAssembly | WebAssembly SIMD | |
---|---|---|---|
Node.js | 1.09459572s | 1.25136047s | 1.50149224s |
Chrome | 1.03628571s | 1.5735s | 1.7085s |
Firefox | 2.36349999s | 1.124s | 1.31383333s |
arm
JavaScript | WebAssembly | WebAssembly SIMD | |
---|---|---|---|
Node.js | 0.64484078s | 0.589527777s | 0.724107999s |
Chrome | 0.64975123s | 0.458300000s | 0.521111111s |
Firefox | 1.42000000s | 0.718375000s | 0.895714285s |
Safari | 4.68739999s | 0.417900000s | 0.513666666s |
ケース 3
x86
JavaScript | WebAssembly | WebAssembly SIMD | |
---|---|---|---|
Node.js | 7.3680s | 4.435281s | 7.368002s |
Chrome | 8.9684s | 10.6116s | 8.5714s |
Firefox | 14.8172s | 4.946800s | 6.42819s |
arm
Safari/JavaScript
において異常な値が観測されたが本筋ではないので Chart からは除外した
JavaScript | WebAssembly | WebAssembly SIMD | |
---|---|---|---|
Node.js | 4.91324333s | 2.771773525s | 2.65507506s |
Chrome | 4.6228s | 2.7876s | 2.737s |
Firefox | 4.355199999s | 1.768666666s | 2.0589999999s |
Safari | 2132.38339999s | 2.70720000s | 2.4135s |
所感
すくなくとも手持ちのx86
とarm
では傾向に差異が見られた。x86
ではJavaScript
が最速となるケースが多く、arm
ではWebAssembly(SIMD無し)
が最速となるケースが多く見られたように思う。
共通して言えるのはWebAssembly SIMD
による高速化は上記のケースではあまり観測されなかった点だ。WebAssembly SIMD
が最速となったのはいずれもケース 3 で、x86
ではChrome
、arm
ではNode.js
,Chrome
,Safari
において最速となった。
これはケース 3 では他のケースより計算量が格段に多くなっている点が影響していそうだと考えた。というのもpixelmatch
はまず色差を計算し、それがしきい値を超えた場合にantialias
の計算を行う。つまり差分が多ければ多いほど計算量は数倍に膨れ上がるためだ。(単純に画像サイズが大きいというのもあるが)
ケース 4
ケース 4 としてサイズが800x578
の真っ白の画像と真っ黒の画像で(とりあえずChrome
,Node.js
のみ)計測してみた。
これは画像サイズが小さくともantialias
計算が多く行われるケースにおいてはSIMD
のほうが高速になるのではないか?という推測からである。
x86
WebAssembly | WebAssembly SIMD | |
---|---|---|
Node.js | 0.02341237s | 0.0309080s |
Chrome | 0.02592276s | 0.0325396s |
arm
WebAssembly | WebAssembly SIMD | |
---|---|---|
Node.js | 0.01476703s | 0.0123578s |
Chrome | 0.01488846s | 0.0124841s |
arm
においてはSIMD
を使用することで20%
程度高速化していることが分かる。
すなわちpixelmatch
が想定しているようなユースケースにおいて行われている程度の計算では速度差がでにくいのだろうと推測される。機械語を見るにv128
に詰め替えるにもオーバーヘッドがあるだろうし、v128
に詰め変えたたあと、ちょろっと乗算をして値を取り出すような処理ではSIMD
のメリットはでにくいのかもしれない。
ただx86
については予想に反して変わらずSIMD
のほうが遅かった。
そこで、更に追加計測として9112×12854
の真っ白の画像と真っ黒の画像で差分をった。 これはケース 3 ではx86
でもSIMD
のほうが高速なケースがあったため、画像の大きさも速度比に影響のある変数であるのではないかと推測したためである。
結果としては以下のように20%
ほど高速化した。つまりx86
においてもSIMD
により高速化するケースはたしかに存在しそうだが、arm
の場合とはまた違ったパラメータがありそうに感じた。
WebAssembly | WebAssembly SIMD | |
---|---|---|
Chrome | 13.6169s | 16.9574s |
なので現時点においてSIMD
による高速化が期待されるケースはありそうだが、少なくともpixelmatch
が想定するユースケースでは多くの場合でSIMD
化は高速化の面では優位に働かない。という話になりそうだ。
この辺は最適化によって状況も変わっていくだろうし、また適用できそうなケースを見つけたらSIMD
を使って遊んでみることにする。
以上。