AssemblyScriptのパフォーマンス測定してみた

AssemblyScriptから吐き出される WebAssemblyのパフォーマンス計測を行った。

概要

AssemblyScriptにて簡単な関数を作成し、WebAssemblyとして吐き出された関数とJavaScriptで定義した関数の実行時間を比較した。

コード

AssemblyScript で引数に任意の32bit 整数を受け取り、その整数分ループする関数を定義した。

export function loop(n: i32): void { while(n--) {} }

このコードを yarn run asbuild でビルドすると wasmwat などのコードを生成する。

またこの時開発環境では最適化されたコードと最適化されていないコードが吐き出される。

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

また brbr_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よりもパフォーマンスが優れると考えていたため、この結果には驚いた。

正直なぜこのような結果になったのかは現状分からない。分かり次第情報を加えようと思う。

©Tsurutan. All Rights Reserved.