nimには,元々JavaScriptバックエンドが用意されていて, nimのプログラムをJavaScriptにコンパイルすることができる. 最近話題の(要出典)Web Assemblyについても,公式にはサポートされていないものの, emscriptenを利用することで,Web Assemblyへ吐き出すことができる.
今回は,nimで実装した,階乗を求める関数をWebAssemblyにコンパイルし,JavaScriptから呼び出してみる.
なお,今回実装したものは musou1500/nim_wasm で公開している.
関数をJavaScriptから呼び出せるようにするため,jsbindを使う.
EMSCRIPTEN_KEEPALIVE
というマクロが提供されており,これを関数に付ければ良い.
この時気をつけるべきなのが,32ビット整数を使うことと,デフォルト引数が使えないことだ. emscriptenから呼び出す関係上,64ビット整数では動作しない.
import jsbind/emscripten
proc fact*(n: int32, acc: int32): int32 {.EMSCRIPTEN_KEEPALIVE.} =
if n <= 1: acc else: fact(n - 1, n * acc)
なお,大抵のCコンパイラであれば末尾呼び出し最適化を行ってくれるので,スタックについての心配はない.
例えば,以下のようなプログラムを書いたとする.
int fact(int n, int acc) {
if (n == 1) {
return acc;
}
return fact(n - 1, n * acc);
}
このプログラムを最適化オプション付きでコンパイルすると,以下のようなアセンブリを吐き出してくれる.
fact: # @fact
.cfi_startproc
# %bb.0:
cmpl $1, %edi
je .LBB0_3
.p2align 4, 0x90
.LBB0_1: # =>This Inner Loop Header: Depth=1
imull %edi, %esi
addl $-1, %edi
cmpl $1, %edi
jne .LBB0_1
.LBB0_3:
movl %esi, %eax
retq
細かい説明は省くが,以下のプログラムと同じだと考えて良い.
int fact(int n, int acc) {
while (n > 1) {
acc = n * acc;
n -= 1;
}
return acc;
}
nimをコンパイルする際,gccやclangの代わりにemccを呼び出したいので,
以下の内容をnim.cfg
として保存しておく.
@if emscripten:
cc = clang
clang.exe = "emcc"
clang.linkerexe = "emcc"
clang.options.linker = ""
cpu = "i386"
warning[GcMem]= off
passC = "-s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS=\"['cwrap']\" -O3"
passL = "-s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS=\"['cwrap']\" -O3"
@end
以下のコマンドを実行すると,nim_wasm.js
とnim_wasm.wasm
が吐き出される.
$ nim c -d:release -d:emscripten -o:nim_wasm.js nim_wasm.nim
emscriptenで出力したプログラムに含まれる関数を呼び出す方法はいくつかあるが,今回はcwrap
を使う.
cwrap
は,以下のように
- 関数名
- 返り値の型
- 引数の型
を指定することで,JavaScriptから呼び出しやすいようにラップしてくれる.
Module['onRuntimeInitialized'] = function() {
const fact = Module.cwrap('fact', 'number', ['number', 'number']);
console.log(nimFact(3, 1)); // 6
};
これを,以下のようにスクリプトを読み込むようにした index.html
を用意すれば実行できる.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="./nim_wasm.js"></script>
<script src="./app.js"></script>
<title></title>
</head>
<body>
</body>
</html>
せっかくWeb Assemblyに吐き出したので,多少は速くなっていて欲しいものだ. そういうわけなので,Benchmark.jsを使ってベンチマークを取る.
Benchmark.jsは,以下のようにcdnから利用できる.
これを先程の index.html
に追加すれば良い.
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/benchmark/2.1.4/benchmark.min.js"></script>
今回は,以下の3つの関数を実装し,パフォーマンスを比較する.
- nimFact nimで実装した関数
- factRec JavaScriptで実装した,再帰を用いて階乗を求める関数
- factLoop JavaScriptで実装した,ループを用いて階乗を求める関数
Module['onRuntimeInitialized'] = function() {
const nimFact = Module.cwrap('fact', 'number', ['number', 'number']);
const factRec = (n, acc) => n <= 1 ? acc : factRec(n - 1, n * acc);
const factLoop = (n, acc) => {
while (n > 1) {
acc = n * acc;
n = n - 1;
}
return acc;
};
const suite = new Benchmark.Suite;
suite.add('nimFact', () => nimFact(10, 1))
.add('factRec', () => factRec(10, 1))
.add('factLoop', () => factLoop(10, 1))
.on('cycle', event => {
console.log(String(event.target));
})
.on('complete', () => {
console.log('Fastest is ' + suite.filter('fastest').map('name'));
})
.run({ 'async': true });
};
結果は以下のようになった.
関数 | ops/sec | サンプル数 |
---|---|---|
nimFact | 10,991,173 ops/sec ±4.23% | 54 |
factRec | 19,453,389 ops/sec ±6.94% | 49 |
factLoop | 98,535,908 op/sec ±4.20% | 52 |
JavaScriptで実装した関数の方が速い結果となった. emscripteに関連して,色々なオーバーヘッドがあるんだろうけど,意外な結果だ.