◆ neon と wasm-pack

処理してるデータ量が多くなってきてやっぱり Node.js だと速度が不利かなーと思ってます
ネイティブモジュールといえば 以前 N-API を使いました

コード量も多いし複雑だし C++ だしであんまり使おうという気もしません
最近ならネイティブ言語は Rust でしょ Rust で Node.js のネイティブモジュール作れないのかなと調べてみました

neon

調べてみたら neon というのがありました
ざっと見た感じ良さそうな印象だったので使ってみることにしました
基本はドキュメントのとおりに進めます
https://neon-bindings.com/docs/getting-started

npm で neon-cli をインストールしたら neon コマンドが使えるようになります

neon new neon-test

で neon-test というプロジェクトフォルダが作られます
中に lib/index.js があってこの JavaScript ファイルが Node.js で実行するエントリポイントになります
また native/src/lib.rs が Rust のファイルです

速度比較したいので簡単に作れて実行時間がかかりそうなものということでフィボナッチ数計算する関数を作ります

#[macro_use]
extern crate neon;

use neon::prelude::*;

fn _fib(x : i32) -> i32 {
if x < 2 {
x
} else {
_fib(x - 1) + _fib(x - 2)
}
}

fn fib(mut cx: FunctionContext) -> JsResult<JsNumber> {
let x = cx.argument::<JsNumber>(0)?.value() as i32;
Ok(cx.number(_fib(x) as f64))
}

register_module!(mut cx, {
cx.export_function("fib", fib)
});

register_module でエクスポートする関数を指定します
JavaScript とのやり取り部分は型変換を挟むので fib 関数はその処理だけにして _fib 関数を計算部分にしています

Rust ファイルのモジュールが完成したらビルドします

neon build

成功したら次はこのモジュールを使う lib/index.js ファイルです

const addon = require("../native")

const list = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
20, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
]

for(const i of list) {
let r
console.time(i)
r = addon.fib(i)
console.timeEnd(i)
console.log(r)
}

適当に 20 種類くらいの数値でフィボナッチ数を計算して処理時間を計測します

require は ../native を指定していますが デフォルトでここが指定されています
neon build が成功すると native フォルダの中に index.node というネイティブアドオン形式のファイルを生成します
native フォルダを指定しているとこのファイルがロードされます

結果は最後にまとめます

それにしてもすごく簡単に書けますね
C++ の苦労はなんだったんでしょう

JavaScript 版

速度比較のために JavaScript で計算するものも用意します

const fib = x => x < 2 ? x : fib(x - 1) + fib(x - 2)

const list = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
20, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
]

for(const i of list) {
let r
console.time(i)
r = fib(i)
console.timeEnd(i)
console.log(r)
}

WebAssembly

最初は neon を使ったネイティブモジュールと JavaScript 版を比較していたのですが そういえば WebAssembly というものがあったなと思い出しました
この比較には WebAssembly も入れておきたいなと思って WebAssembly もやってみることにしました

やり方は MDN のページに沿ってやります
https://developer.mozilla.org/en-US/docs/WebAssembly/Rust_to_wasm

ツールは wasm-pack というものを使います
neon のときは特にエラーもなくスムーズだったのですがこっちはちょっとつまづくところがありました
原因と対処方法を先に書いておきます

ビルドエラーの場合

build 時に openssl 関係のビルドで失敗したらいくつか OS にパッケージのインストールが必要です
私はググってでてきたこのページを参考に次のコマンドを実行しました

sudo dnf install pkg-config openssl-devel

また wasm-pack の build では Rustup が必要です
dnf でインストールした rustc と cargo だけだとエラーになりました
一応 Rustup を使わない方法もあります
しかし私の環境だとうまくいきませんでした

rustc のバージョンに応じたライブラリを手動でダウンロードして lib フォルダに配置するというものです
バージョンは一致しているのに互換性のないバージョンというエラーになりました

Rustup 入れたほうが楽そうだったので Rustup に入れ替えました
Rustup を入れるときに rustc コマンドを削除する必要があって dnf で入れた rust パッケージを削除すると rustc や cargo コマンドは削除されます

wasm-pack

cargo で wasm-pack をインストールします
プロジェクトフォルダは cargo コマンドで作ります

cargo new --lib fib

wasm-pack にも new コマンドはあるみたいですが MDN のやり方に合わせて cargo new にしました

プロジェクトフォルダが作られたら Cargo.toml に下のコードを追記します

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2"

こういう部分は wasm-pack new だと最初からやってくれるのかもしれません

次は src/lib.rs に Rust のコードを書きます

extern crate wasm_bindgen;

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn fib(x : i32) -> i32 {
if x < 2 {
x
} else {
fib(x - 1) + fib(x - 2)
}
}

neon に比べるとかなり短いです
引数の変換部分は自動でやってくれるようで 内部でやりたいことだけ書けばいい感じです

次は build です
--target で Node.js を指定しないとうまく動きませんでした
neon と違ってブラウザなども対応してる WebAssembly なのでビルドターゲットの指定が必要みたいです

wasm-pack build --target=nodejs

成功したらいくつかファイルができています
.d.ts は TypeScript 用の型定義なので 無視すると fib.js ファイルと fib_bg.wasm ファイルがあります
fib の部分が cargo new で指定したプロジェクト名になります
fib_bg.wasm が WebAssembly のモジュールです

fib.js には使い方説明のようなサンプルコードが書かれています
バイナリデータになると JavaScript ファイルのように直接見て関数一覧や引数とかを知ることができないので単純に引数をそのまま渡すだけでも JavaScrpt 関数通したモジュールにしておいたほうが良いのかもしれません
ですが 今回は必要ないので fib_bg.wasm を直接使います

const wasm = require("./fib_bg")

const list = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
20, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
]

for(const i of list) {
let r
console.time(i)
r = wasm.fib(i)
console.timeEnd(i)
console.log(r)
}

これで WebAssembly 版も完成です

速度比較

JavaScript 関数と Node.js 用のネイティブモジュールと WebAssembly モジュールの 3 種類が用意できたので速度比較してみます

使ったバージョンは⇩のものです

node v13.9.0
rustc 1.41.1 (f3e1a954d 2020-02-24)
neon 0.3.3
wasm-pack 0.9.1

それぞれ 3 回実行しています

node(1)node(2)node(3)neon(1)neon(2)neon(3)wasm(1)wasm(2)wasm(3)
10.118ms0.116ms0.115ms0.101ms0.108ms0.102ms0.106ms0.105ms0.104ms
20.009ms0.009ms0.009ms0.012ms0.033ms0.012ms0.009ms0.009ms0.009ms
30.008ms0.008ms0.008ms0.01ms0.011ms0.01ms0.008ms0.008ms0.008ms
40.008ms0.011ms0.008ms0.01ms0.011ms0.01ms0.008ms0.007ms0.007ms
50.009ms0.009ms0.009ms0.012ms0.012ms0.011ms0.015ms0.016ms0.016ms
60.016ms0.015ms0.015ms0.016ms0.017ms0.016ms0.021ms0.021ms0.039ms
70.007ms0.007ms0.007ms0.008ms0.008ms0.008ms0.005ms0.005ms0.005ms
80.008ms0.008ms0.008ms0.008ms0.007ms0.008ms0.005ms0.005ms0.005ms
90.01ms0.01ms0.01ms0.008ms0.008ms0.007ms0.005ms0.005ms0.005ms
100.014ms0.014ms0.014ms0.009ms0.009ms0.009ms0.006ms0.006ms0.006ms
202.138ms2.094ms2.088ms0.095ms0.111ms0.095ms0.073ms0.073ms0.073ms
3012.262ms12.22ms12.375ms11.222ms11.328ms10.732ms8.329ms8.269ms8.47ms
3119.715ms19.755ms19.699ms17.297ms18.553ms17.674ms13.359ms13.415ms13.363ms
3232.003ms31.876ms31.874ms28.061ms29.338ms28.129ms21.637ms21.595ms21.611ms
3351.789ms52.301ms51.97ms45.91ms50.654ms45.419ms35.022ms34.957ms35.017ms
3483.71ms83.905ms83.63ms73.254ms77.331ms73.257ms56.746ms56.876ms56.705ms
35135.942ms135.141ms135.344ms118.679ms126.182ms120.153ms91.542ms91.641ms91.799ms
36219.124ms219.048ms219.493ms191.846ms194.032ms192.531ms148.774ms148.505ms149.4ms
37355.084ms356.036ms355.162ms311.393ms313.962ms311.448ms240.369ms240.654ms242.841ms
38575.046ms573.727ms573.647ms503.318ms504.265ms508.84ms388.511ms388.686ms388.804ms
39929.794ms928.654ms930.541ms815.072ms814.493ms814.212ms628.882ms630.407ms629.248ms
401.504s1.503s1.508s1.317s1.320s1.319s1.018s1.043s1.024s

多くて見づらいので 種類ごとの平均値にまとめました

nodeneonwasm
10.116ms0.104ms0.105ms
20.009ms0.019ms0.009ms
30.008ms0.01ms0.008ms
40.009ms0.01ms0.007ms
50.009ms0.012ms0.016ms
60.015ms0.016ms0.027ms
70.007ms0.008ms0.005ms
80.008ms0.008ms0.005ms
90.01ms0.008ms0.005ms
100.014ms0.009ms0.006ms
202.107ms0.1ms0.073ms
3012.286ms11.094ms8.356ms
3119.723ms17.841ms13.379ms
3231.918ms28.509ms21.614ms
3352.02ms47.328ms34.999ms
3483.748ms74.614ms56.776ms
35135.476ms121.671ms91.661ms
36219.222ms192.803ms148.893ms
37355.427ms312.268ms241.288ms
38574.14ms505.474ms388.667ms
39929.663ms814.592ms629.512ms
401.505s1.319s1.028s

JavaScript より Rust を使った部分のほうがやっぱり速いです
neon と WebAssembly の比較では neon よりも wasm のほうが速いです
ブラウザでも使える汎用的な WebAssembly よりも Node.js 専用の機能が使われてる neon のほうが速いのかもと思ったのに そんなことはなかったです

今回は使ってませんが wasm-pack では JavaScript の関数を受け取って Rust 側でそれを実行できるみたいです
MDN の例だと alert を使っています
そういう事もできる上に コードを見ても wasm のほうがスッキリしてますし Node.js で Rust を使うなら WebAssembly モジュールを使うのがよさそうですね