JavaScript
Haskell
Emscripten
WebAssembly
adventcalendar2017

目的

これは WebAssembly Advent Calendar 2017 23日目の記事です。

昨年、haskellのインタプリタのhugsをemscriptenを用いさまざまな困難がありましたが、printfデバッグで無事javascript(web版, nodejs版)にしてきました。

その後、firefox quantumによって上記のjavascriptは申し分ないほど高速化されたのですが、WebAssemblyによってさらに高速化できないか確認したくなり、今回asm.jsからWebAssemblyへの移行を行いました。

asm.jsからWebAssemblyへの移行とその問題

これがasm.jsからWebAssemblyへの移行に必要だった変更(PR)でした。

まとめると下記の5つです。

1, dlopenで読み込むshared libraryがjsからwasmになっている。
2, Module['locateFile']でwasmの読み込むパスを指定する。
3, wasmファイルのなかではEM_ASMできない。
4, wasmのロードではすべてのシンボルが解決できてないといけない。
5, emscriptenの吐き出すhtml(wasmをロードする関数が入っている)がasm.js版と違うので更新が必要。

dlopenで読み込むshared libraryがjsからwasmに

asm.jsからwasmになり、関数テーブルのフォーマットが変わりました。
関数テーブルはWebAssemblyオブジェクトのtableになり、従来のsignature(関数の型のようなもの)ごとに異なる配列であった関数テーブルよりスリムになりました。
内部の関数の構造がことなり、従来のshared libraryと互換性がなくなり、asm.jsとwasmは両立はできません。
wasmの形式のshared libraryに切り替えましょう。

wasmを読み込むパスの管理

shared libraryを使う側のファイルを-s WASM=1をつけてコンパイルするとjsとwasmの両方のファイルが出力されます。しかし、node "jsの置いてあるディレクトリ"/hoge.jsのように実行してもwasmファイルを読み込んでくれません。

APIリファレンスにありますが、Module['locateFile']を設定しましょう。

shared libraryでEM_ASMが使えない

EM_ASMはC言語の中に埋め込まれたjsを呼び出す関数ですが、wasmの中では外部のjsの関数は呼べますがjsは評価できないので使えません。shared libraryなのにemscriptenの世界のアセンブラであるjsが呼べないという何とも拡張性のないものになってしました。
必要に応じて外部のjsで下記のようなEM_ASMをエミュレートする関数を用意しました。
実行時にevalされるので速度はもとのEM_ASMより遅いと予想されます。

function em_asm_0(exp){
  eval(Pointer_stringify(exp));
}
function em_asm_1(exp,arg0){
  var $0=arg0;
  eval(Pointer_stringify(exp));
}

wasmのシンボル解決

asm.jsのときは実行時にシンボル解決して、使ってない関数のシンボルが解決できなくても問題ありませんでした。しかし、wasmになってwasmファイルをロードするときにすべてのシンボルが解決できるかチェックするようになりました。実行する前にチェックするのでデバッグしやすくなるため悪くない動作ですが、asm.jsのときとは違うのでシンボル解決のデバッグを今回行いました。

初期化方法

webでemscriptenのファイルを読む方法がasm.jsとwasmで違いますが、生成されたhtmlをみて直しましょう。

ベンチマーク

web版はfirefox quantumで起動時間を見ました。chromeだとwebassembly.compileを使っていないのにエラーがでて起動できませんでした。起動時間はwasmにすると遅くなりました。
アセットのほうが多いためwasmにした効果がまったく出てないです。

# asm.js版
https://junjihashimoto.github.io/hugs-js/
Finishまで7.61s
# wasm版
https://junjihashimoto.github.io/hugs-js/wasm/
Finishまで9.03s

nodejs版はasm.jsとwasmでインタプリタの起動時間を見てみました。
wasmの方は半分の時間で起動できました。

# asm.js版
$ time ./node_modules/.bin/runhugs ../shyougi-app/Shyougi.hs
123

real    0m9.799s
user    0m11.281s
sys     0m2.203s
# wasm版
$ time ../hugs-js/bin/runhugs ../shyougi-app/Shyougi.hs
123

real    0m5.789s
user    0m6.141s
sys     0m1.688s

まとめ

haskellのインタプリタであるhugsをasm.jsで作っていたものをwasmへ移行しました。
shared libraryに関する問題がありましたが、無事解決できました。
ブラウザでは移行後起動が遅くなりましたが、nodejsでは半分の時間で起動できるようになりました。