Node.js で Rust を使う
◆ neon と wasm-pack
処理してるデータ量が多くなってきてやっぱり Node.js だと速度が不利かなーと思ってます
ネイティブモジュールといえば 以前 N-API を使いました
コード量も多いし複雑だし C++ だしであんまり使おうという気もしません
最近ならネイティブ言語は Rust でしょ Rust で Node.js のネイティブモジュール作れないのかなと調べてみました
ざっと見た感じ良さそうな印象だったので使ってみることにしました
基本はドキュメントのとおりに進めます
https://neon-bindings.com/docs/getting-started
npm で neon-cli をインストールしたら neon コマンドが使えるようになります
で neon-test というプロジェクトフォルダが作られます
中に lib/index.js があってこの JavaScript ファイルが Node.js で実行するエントリポイントになります
また native/src/lib.rs が Rust のファイルです
速度比較したいので簡単に作れて実行時間がかかりそうなものということでフィボナッチ数計算する関数を作ります
register_module でエクスポートする関数を指定します
JavaScript とのやり取り部分は型変換を挟むので fib 関数はその処理だけにして _fib 関数を計算部分にしています
Rust ファイルのモジュールが完成したらビルドします
成功したら次はこのモジュールを使う lib/index.js ファイルです
適当に 20 種類くらいの数値でフィボナッチ数を計算して処理時間を計測します
require は ../native を指定していますが デフォルトでここが指定されています
neon build が成功すると native フォルダの中に index.node というネイティブアドオン形式のファイルを生成します
native フォルダを指定しているとこのファイルがロードされます
結果は最後にまとめます
それにしてもすごく簡単に書けますね
C++ の苦労はなんだったんでしょう
この比較には WebAssembly も入れておきたいなと思って WebAssembly もやってみることにしました
やり方は MDN のページに沿ってやります
https://developer.mozilla.org/en-US/docs/WebAssembly/Rust_to_wasm
ツールは wasm-pack というものを使います
neon のときは特にエラーもなくスムーズだったのですがこっちはちょっとつまづくところがありました
原因と対処方法を先に書いておきます
私はググってでてきたこのページを参考に次のコマンドを実行しました
また wasm-pack の build では Rustup が必要です
dnf でインストールした rustc と cargo だけだとエラーになりました
一応 Rustup を使わない方法もあります
しかし私の環境だとうまくいきませんでした
rustc のバージョンに応じたライブラリを手動でダウンロードして lib フォルダに配置するというものです
バージョンは一致しているのに互換性のないバージョンというエラーになりました
Rustup 入れたほうが楽そうだったので Rustup に入れ替えました
Rustup を入れるときに rustc コマンドを削除する必要があって dnf で入れた rust パッケージを削除すると rustc や cargo コマンドは削除されます
プロジェクトフォルダは cargo コマンドで作ります
wasm-pack にも new コマンドはあるみたいですが MDN のやり方に合わせて cargo new にしました
プロジェクトフォルダが作られたら Cargo.toml に下のコードを追記します
こういう部分は wasm-pack new だと最初からやってくれるのかもしれません
次は src/lib.rs に Rust のコードを書きます
neon に比べるとかなり短いです
引数の変換部分は自動でやってくれるようで 内部でやりたいことだけ書けばいい感じです
次は build です
--target で Node.js を指定しないとうまく動きませんでした
neon と違ってブラウザなども対応してる WebAssembly なのでビルドターゲットの指定が必要みたいです
成功したらいくつかファイルができています
.d.ts は TypeScript 用の型定義なので 無視すると fib.js ファイルと fib_bg.wasm ファイルがあります
fib の部分が cargo new で指定したプロジェクト名になります
fib_bg.wasm が WebAssembly のモジュールです
fib.js には使い方説明のようなサンプルコードが書かれています
バイナリデータになると JavaScript ファイルのように直接見て関数一覧や引数とかを知ることができないので単純に引数をそのまま渡すだけでも JavaScrpt 関数通したモジュールにしておいたほうが良いのかもしれません
ですが 今回は必要ないので fib_bg.wasm を直接使います
これで WebAssembly 版も完成です
使ったバージョンは⇩のものです
それぞれ 3 回実行しています
多くて見づらいので 種類ごとの平均値にまとめました
JavaScript より Rust を使った部分のほうがやっぱり速いです
neon と WebAssembly の比較では neon よりも wasm のほうが速いです
ブラウザでも使える汎用的な WebAssembly よりも Node.js 専用の機能が使われてる neon のほうが速いのかもと思ったのに そんなことはなかったです
今回は使ってませんが wasm-pack では JavaScript の関数を受け取って Rust 側でそれを実行できるみたいです
MDN の例だと alert を使っています
そういう事もできる上に コードを見ても wasm のほうがスッキリしてますし Node.js で Rust を使うなら WebAssembly モジュールを使うのがよさそうですね
ネイティブモジュールといえば 以前 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) | |
---|---|---|---|---|---|---|---|---|---|
1 | 0.118ms | 0.116ms | 0.115ms | 0.101ms | 0.108ms | 0.102ms | 0.106ms | 0.105ms | 0.104ms |
2 | 0.009ms | 0.009ms | 0.009ms | 0.012ms | 0.033ms | 0.012ms | 0.009ms | 0.009ms | 0.009ms |
3 | 0.008ms | 0.008ms | 0.008ms | 0.01ms | 0.011ms | 0.01ms | 0.008ms | 0.008ms | 0.008ms |
4 | 0.008ms | 0.011ms | 0.008ms | 0.01ms | 0.011ms | 0.01ms | 0.008ms | 0.007ms | 0.007ms |
5 | 0.009ms | 0.009ms | 0.009ms | 0.012ms | 0.012ms | 0.011ms | 0.015ms | 0.016ms | 0.016ms |
6 | 0.016ms | 0.015ms | 0.015ms | 0.016ms | 0.017ms | 0.016ms | 0.021ms | 0.021ms | 0.039ms |
7 | 0.007ms | 0.007ms | 0.007ms | 0.008ms | 0.008ms | 0.008ms | 0.005ms | 0.005ms | 0.005ms |
8 | 0.008ms | 0.008ms | 0.008ms | 0.008ms | 0.007ms | 0.008ms | 0.005ms | 0.005ms | 0.005ms |
9 | 0.01ms | 0.01ms | 0.01ms | 0.008ms | 0.008ms | 0.007ms | 0.005ms | 0.005ms | 0.005ms |
10 | 0.014ms | 0.014ms | 0.014ms | 0.009ms | 0.009ms | 0.009ms | 0.006ms | 0.006ms | 0.006ms |
20 | 2.138ms | 2.094ms | 2.088ms | 0.095ms | 0.111ms | 0.095ms | 0.073ms | 0.073ms | 0.073ms |
30 | 12.262ms | 12.22ms | 12.375ms | 11.222ms | 11.328ms | 10.732ms | 8.329ms | 8.269ms | 8.47ms |
31 | 19.715ms | 19.755ms | 19.699ms | 17.297ms | 18.553ms | 17.674ms | 13.359ms | 13.415ms | 13.363ms |
32 | 32.003ms | 31.876ms | 31.874ms | 28.061ms | 29.338ms | 28.129ms | 21.637ms | 21.595ms | 21.611ms |
33 | 51.789ms | 52.301ms | 51.97ms | 45.91ms | 50.654ms | 45.419ms | 35.022ms | 34.957ms | 35.017ms |
34 | 83.71ms | 83.905ms | 83.63ms | 73.254ms | 77.331ms | 73.257ms | 56.746ms | 56.876ms | 56.705ms |
35 | 135.942ms | 135.141ms | 135.344ms | 118.679ms | 126.182ms | 120.153ms | 91.542ms | 91.641ms | 91.799ms |
36 | 219.124ms | 219.048ms | 219.493ms | 191.846ms | 194.032ms | 192.531ms | 148.774ms | 148.505ms | 149.4ms |
37 | 355.084ms | 356.036ms | 355.162ms | 311.393ms | 313.962ms | 311.448ms | 240.369ms | 240.654ms | 242.841ms |
38 | 575.046ms | 573.727ms | 573.647ms | 503.318ms | 504.265ms | 508.84ms | 388.511ms | 388.686ms | 388.804ms |
39 | 929.794ms | 928.654ms | 930.541ms | 815.072ms | 814.493ms | 814.212ms | 628.882ms | 630.407ms | 629.248ms |
40 | 1.504s | 1.503s | 1.508s | 1.317s | 1.320s | 1.319s | 1.018s | 1.043s | 1.024s |
多くて見づらいので 種類ごとの平均値にまとめました
node | neon | wasm | |
---|---|---|---|
1 | 0.116ms | 0.104ms | 0.105ms |
2 | 0.009ms | 0.019ms | 0.009ms |
3 | 0.008ms | 0.01ms | 0.008ms |
4 | 0.009ms | 0.01ms | 0.007ms |
5 | 0.009ms | 0.012ms | 0.016ms |
6 | 0.015ms | 0.016ms | 0.027ms |
7 | 0.007ms | 0.008ms | 0.005ms |
8 | 0.008ms | 0.008ms | 0.005ms |
9 | 0.01ms | 0.008ms | 0.005ms |
10 | 0.014ms | 0.009ms | 0.006ms |
20 | 2.107ms | 0.1ms | 0.073ms |
30 | 12.286ms | 11.094ms | 8.356ms |
31 | 19.723ms | 17.841ms | 13.379ms |
32 | 31.918ms | 28.509ms | 21.614ms |
33 | 52.02ms | 47.328ms | 34.999ms |
34 | 83.748ms | 74.614ms | 56.776ms |
35 | 135.476ms | 121.671ms | 91.661ms |
36 | 219.222ms | 192.803ms | 148.893ms |
37 | 355.427ms | 312.268ms | 241.288ms |
38 | 574.14ms | 505.474ms | 388.667ms |
39 | 929.663ms | 814.592ms | 629.512ms |
40 | 1.505s | 1.319s | 1.028s |
JavaScript より Rust を使った部分のほうがやっぱり速いです
neon と WebAssembly の比較では neon よりも wasm のほうが速いです
ブラウザでも使える汎用的な WebAssembly よりも Node.js 専用の機能が使われてる neon のほうが速いのかもと思ったのに そんなことはなかったです
今回は使ってませんが wasm-pack では JavaScript の関数を受け取って Rust 側でそれを実行できるみたいです
MDN の例だと alert を使っています
そういう事もできる上に コードを見ても wasm のほうがスッキリしてますし Node.js で Rust を使うなら WebAssembly モジュールを使うのがよさそうですね