概要
AssemblyScriptにて簡単な関数を作成し、WebAssemblyとして吐き出された関数とJavaScriptで定義した関数の実行時間を比較した。
コード
AssemblyScript で引数に任意の32bit 整数を受け取り、その整数分ループする関数を定義した。
export function loop(n: i32): void { while(n--) {} }
このコードを yarn run asbuild
でビルドすると wasm
や wat
などのコードを生成する。
またこの時開発環境では最適化されたコードと最適化されていないコードが吐き出される。
wasm
だと何が書かれているか分からないため wat
を載せる。
最適化前
(module (type $i32_=>_none (func (param i32))) (memory $0 0) (table $0 1 funcref) (global $~lib/memory/__data_end i32 (i32.const 8)) (global $~lib/memory/__stack_pointer (mut i32) (i32.const 16392)) (global $~lib/memory/__heap_base i32 (i32.const 16392)) (export "loop" (func $assembly/index/loop)) (export "memory" (memory $0)) (func $assembly/index/loop (param $0 i32) (local $1 i32) loop $while-continue|0 local.get $0 local.tee $1 i32.const 1 i32.sub local.set $0 local.get $1 local.set $1 local.get $1 if br $while-continue|0 end end ) )
最適化後
(module (type $i32_=>_none (func (param i32))) (memory $0 0) (export "loop" (func $assembly/index/loop)) (export "memory" (memory $0)) (func $assembly/index/loop (param $0 i32) (local $1 i32) loop $while-continue|0 local.get $0 local.tee $1 i32.const 1 i32.sub local.set $0 local.get $1 br_if $while-continue|0 end ) )
最適化前と最適化後のコードを比較してみると下記コードが最適化後のコードから消えていることが分かる。
(table $0 1 funcref) (global $~lib/memory/__data_end i32 (i32.const 8)) (global $~lib/memory/__stack_pointer (mut i32) (i32.const 16392)) (global $~lib/memory/__heap_base i32 (i32.const 16392))
local.set $1 local.get $1
また br
が br_if
へと変換されている。
if br $while-continue|0 en
br_if $while-continue|
このコードをJavaScriptから読み込めるように下記コードを追加した。
function normalLoop(n) { while(n--) {} } (async () => { let { instance: { exports: { loop: optimizedLoop } }} = await WebAssembly.instantiateStreaming(fetch('build/optimized.wasm')) let { instance: { exports: { loop: untouchedLoop } }} = await WebAssembly.instantiateStreaming(fetch('build/untouched.wasm')) for(let n of [5, 100000, 2147483647]) { console.log("n = %d", n); for(let fun of [optimizedLoop, untouchedLoop, normalLoop]) { let start = performance.now(); fun(n) let end = performance.now(); console.log(`${fun.name} : time = ${end - start} ms.`); } } })()
結果
上記のコードを実行すると下記のような結果になった。
as.js:12 n = 5 as.js:17 0 : time = 0 ms. as.js:17 0 : time = 0.004999994416721165 ms. as.js:17 normalLoop : time = 0.025000001187436283 ms. as.js:12 n = 100000 as.js:17 0 : time = 0.4599999956553802 ms. as.js:17 0 : time = 0.4450000124052167 ms. as.js:17 normalLoop : time = 5.674999993061647 ms. as.js:12 n = 2147483647 as.js:17 0 : time = 3541.1949999979697 ms. as.js:17 0 : time = 3444.7950000030687 ms. as.js:17 normalLoop : time = 2669.580000001588 ms.
nが5の時最適化されたコードが最も早かった。
この時WebAssemblyで記述したコードがJavaScriptで書いたコードよりもパフォーマンスが優れていることが分かる。
nが100000の時、nが5の時と同様にWebAssemblyのほうが早いが僅差で最適化前のコードが最も早い。
nが2147483647のとき今までの結果とは逆にJavaScriptで書いたコードが最も早かった。
考察
nがある程度小さいときWebAssemblyのほうがパフォーマンスが優れるが、ある閾値を超えるとJavaScriptのほうが早くなる。
もともとWebAssemblyのほうがより多くの計算をするほどJavaScriptよりもパフォーマンスが優れると考えていたため、この結果には驚いた。
正直なぜこのような結果になったのかは現状分からない。分かり次第情報を加えようと思う。