Islands in the byte stream

Technical notes by a software engineer

WebAssemblyをNodeJS Native Addonの配布形式として使う

三行まとめ

  • 高い圧縮率をほこる gzipライブラリ zopfliをwasmにビルドして npmjs.org にリリースしてみた
  • wasmはネイティブコードと比較してだいたい50%くらいの性能を期待できる
  • emscriptenは N-API と比べると出来ることが少なすぎるのが課題

背景

WebAssembly *1 の評価のために、Cで書かれたzlib互換の圧縮ライブラリ google/zopfli をemscriptenでwasmにビルドしてリリースしてみました。

評価だけでなくリリースまでしたのは、zopfliのnodejs native addonである node-zopfli をしばしばインストールできないことがあるという問題があってそれを解決したかったからです。nodejsのnative addonはnode-gypというビルドツールを使うことが多いようですが、これはPython 2.x などnodejsのエコシステム外のツールを利用するため、npm packageのインストールが難しいことがあるのです*2

WebAssemblyにビルドしたものを配布するのであれば、実質的にnaive addonに実行可能形式を含めてnpmjs.orgでま配信するということになり、nodejs native addonのもつビルド問題とは無縁でいられます。これでWebAssemblyの速度が十分であれば、 Cで書かれたライブラリのbindingをマシンに依存しない移植性のある実行可能形式で配布する という夢のようなことを実現できるというわけです。

成果物

github.com

npm install -D "@gfx/zopfli" でインストールできます。 compression-webpack-plugin + node-zopfli でgzip圧縮ファイルを用意する - Islands in the byte stream で示した require("node-zopfli")require("@gfx/zopfli") に置きかえるだけで動くはずです。

ランダムなバイト列を対象にベンチマークをしてみたところ、以下のようになりました(READMEから抜粋):

payload size: 1

universal-zopfli x 106 ops/sec ±0.79% (80 runs sampled) node-zopfli x 201 ops/sec ±1.99% (82 runs sampled)

payload size: 1024

universal-zopfli x 1.37 ops/sec ±12.99% (11 runs sampled) node-zopfli x 4.62 ops/sec ±3.34% (27 runs sampled)

payload size: 1038336

universal-zopfli x 0.26 ops/sec ±6.91% (6 runs sampled) node-zopfli x 0.39 ops/sec ±1.35% (6 runs sampled)

ペイロードサイズによってパターンが違いますが、native binding版の node-zopfli の 30%から70%くらいの速度が出ています。 zopfli に関していえばこのくらいの速度で十分なので、そのままリリースすることにしました*3。しかもこれはNodeJS/V8にWebAssemblyが組み込まれたごく初期のもので、WebAssembly自体まだMVP (Minimum Viable Product) でしかありません。速度はこれから向上していくことでしょう。

なおemscriptenの共通ランタイムがかなり大きく、minifiedで50kbもあります。今回はNodeJSライブラリとして主に使うことを想定しているので関係ありませんが、ウェブ用として考えるとwasmのために50kbがもれなくついてくるというのは小さくない欠点です。この意味でも、現状ではNodeJS用としてはすでに実用可能でも、ちょっとしたウェブ用ライブラリのためにwasmを使える状態ではなさそうです。

しかしNodeJS用の技術としては、WebAssemblyは将来的に移植性のある実行可能形式として非常に有効である可能性を示せたと思います。

Binding Code

さてWebAssemblyは移植性のある実行可能形式としてすでに実用可能ではありますが、現状だと開発が非常に難しいという問題があります。

まず今回は、ある程度枯れているemscriptenを使うことにしました *4

emscriptenでzopfliをビルドする試みはすでに 2013年に imaya/zopfli.js として存在するため、Zopfli を Emscripten で移植した際の備忘録 : document やzopfli.jsのソースコードなどを参考にしつつ開発しました。

しかし開発は正直いってかなり難しかったです。wasm (ソースコード的にはC言語の関数)に対してはポインタも含む数値しか渡せないので、JSの文字列をutf8にencodeしつつ整数の配列にして *5、それをemscriptenのヒープにコピーしてポインタ(=ヒープのオフセット)を得てそれを引数とし、戻り値はこれまたポインタをいじってヒープの内容を Uint8Array に変換し、最後に確保したヒープを freeで解放し…という具合です。いかにもオーバーヘッドがありそうなことばかりしてますね。うまくいかないときのエラーメッセージもわかりにくく、単にbindingを作るならN-APIのほうが開発はしやすいでしょう。

コードはこんな感じです。

binding.c はともかく、 index.ts には苦戦の後がしのばれると思います。

さすがにこのままだとつらいので、そのうちemscripten的にもWebAssembly的にも何とかなるとは思います。とはいえ現状でも文字列(またはバイト列)を受け取りバイト列を返すことはがんばればできるので、すでにそれなりに面白いことはできそうです。

今後の展開

WebAssemblyは Web と付くのでブラウザ専用技術かと思いきや、NodeJSでもかなり有用な技術になる可能性があります。現時点でもNodeJSはスクリプト言語として高速な部類であり、WebAssemblyはそれを補完する技術として期待が持てます。

NodeJSの欠点としてよく語られてきたRDBMSと相性が悪いという特徴は、Promiseとasync/awaitによって解消されました。静的型がないという欠点も、TypeScriptによってある程度解消し、大規模な開発にも耐えられるようになりました。そしてこのWebAssemblyによって、一部の重い処理をCで実装しつつさらにそれをサーバーとウェブブラウザ両方で利用したりするということもできるようになりつつあります。これだけ好条件がそろえば、 NodeJSは今後、ウェブアプリケーション開発用言語として有力になっていくでしょうね!

そういうわけで、WebAssemblyは非常に注目している技術なのです。

*1:See also WebAssembly の基礎 - nmi.jp

*2:node-zopfliはさらにnode-pre-gypというビルド済バイナリをクラウドストレージに保存して利用可能であれば利用する、という仕組みなのですが、これのどこかでコケていてインストールできないことがあるようでした。ただし今回はnode-pre-gypには関心がないので深入りはしてません。

*3:ただしWebAssemblyが利用可能であるNodeJS 8.0以上が必要です。

*4:他、LLVMでwasmを生成したり Cheerp を使うという方法もあるようですが、現時点で一番安定しているのはおそらくemscriptenです。

*5:これはemscriptenが提供するユーティリティ intArrayFromString() でできますが…。