久々にScalaの世界からJSの世界に帰ってきました。
#1. 本日の課題
本来Node.jsは非同期処理をストイックに突き詰めることでマルチスレッドやマルチプロセスのようなオーバーヘッドを伴う方法よりも高効率に並列処理を実現しているわけです。
ただし、それが有効なのは頻繁に「待ち」≒「I/O処理」が発生する場合に限られます。
ひたすらI/OなしでCPUをぶん回す処理、を複数同時にやれって言われたらシングルスレッドなNodeはマルチスレッドに勝てません。
ですがですが、Nodeにだってマルチスレッド/マルチプロセスの仕組みはあります。
さて今回は、
- 数百MB~数GB程度のデータ構造(変数として持っている)に対して
- 秒オーダーの時間をかけておこなう検索処理を
- 複数回おこなう
- 元のデータ構造は更新しない(Read Only)
という要件で、この「複数回おこなう」というところをマルチスレッドかマルチプロセスで並列化して時間を短縮したい、というお題になります。1
2.候補
child_process/cluster
どちらもマルチプロセスなアプローチです。
マルチプロセスなので、メモリは各自が持ちます2。GB単位のメモリをそれぞれのプロセスが持ってたらちょっともったいないですね3。ということで早々に候補から排除。
worker_threads
今回の本命です。
本家にはこのように書いてあります。
child_processやclusterとは異なり、worker_threadsはメモリを共有できます。
ふむふむ、スレッドですからね。そりゃそうですよね。ですが続けてこうも書いてあります。
これは、ArrayBufferインスタンスを転送するか、SharedArrayBufferインスタンスを共有することにより行われます。
What's?
つまり、何も考えなくてもメモリが共有されるわけじゃないようですよ。
それが今回の本題というわけです。
3. Bufferを使うということ
ArrayBuffer/SharedArrayBufferというのはつまり中身はバイナリですから、
{
"こんな": 1,
"好き勝手な": "aaa",
"形をした": [
"JSONから",
"作った",
"object/arrayなんか",
],
"面倒みないよ": true
}
てなもんですよ。最後のtrueがなんだかムカつきますね。
なんとかBufferの基本的な使い方は、Uint32ArrayみたいなTypedArrayをViewとして使うことが多いんではないかと思います。4
メモリは大事にしたい(共有したい)、JSON-likeな構造も扱いたい、そんなわがままに答えてくれるものはないでしょうか。
JSON
メインスレッドでJSON.stringifyしてBufferに載せて、ワーカースレッドでJSON.parseする・・・えーと、parseしてしまったらメモリの共有になってません。ダメです。
JSON以外のシリアライザ
messagePackとかありますね。これもデコードせねばならないのでメモリの共有にはなりません。messagePackならJSONよりエンコード状態のサイズが若干小さいという利点はありますが、デコードしてしまうのでささいな差ですね。5
4. ないものは自分で作ればいいじゃない
そう、それ!
やっと本題だ。
要するに、
- SharedArrayBufferに載せられて
- デコードせずとも中身にObjectやArrayのようにアクセスできる
何かを作ってしまえばいいじゃないか、という話。
「デコードせずとも中身にObjectやArrayのようにアクセスできる」?は?
と思ったあなた!
あるんですよ、JSにはいにしえより伝わる黒魔術、その名もProxyが。
つまり、「Object(あるいはArray)に見える」Proxyが動的にBufferをデコードしてあげればいいんじゃないか、と思ったわけです。
この用途だとJSONやmessagePackなどのシリアライザでエンコードしておいて、SharedArrayBufferに載せて、ワーカー側では全体のデコードは「せず」に読み出しの時だけ必要部分をエンコードするようにすればいいわけですが、ここでもう一つ問題が。
JSONは、全体をスキャンしないと、構造が分からない。keyがあるのかどうか、あった場合に何byte目に入っているのかは、先頭から順に追っていかないとわからない。メモリが節約できても、それではあまりに遅すぎる。messagePackも「ほぼバイナリ形式のJSON」であり同じこと。
そういうわけですから、
- Objectならkeyの有無、Arrayなら配列のサイズがすぐに分かって、
- 値の格納場所にダイレクトにたどり着ける
そんなデータ構造のシリアライザが欲しい!
ということで作ってみましたよ。
使い方(sample.js)もGistに載せてますが一応再掲。
一般的なシリアライザ(JSON/messagePack等)と同じようにごくシンプル。
const roJson = require('./roJson');
const buffer = roJson.encode({a: "Hello roJson.", b: 1, c: [0, 100, 200]});
const proxiedObject = roJson.getProxy(buffer);
console.log(proxiedObject.a); // "Hello roJson."
console.log(proxiedObject.c[1]); // 100
長くなったのでいったんここまで。
工夫ポイント、ベンチマークなどは次回!
-
そんなヘビーな用途にNode.jsを使うなんて、なんてツッコミはなしの方向で。 ↩
-
昔の知識なので最新は違うかもしれないしOSにもよるんでしょうが、プロセスをforkするとすぐにはメモリはコピーされなくて(つまり共有されていて)、書き込んだ時点でページ単位でコピーしてそれぞれの道を歩むようになってました。大部分のメモリに書き込みが発生しないならそれに頼るのも一つの見識ですね。 ↩
-
そういうデータを使うのって機械学習とかCG系とかのイメージ。そういう用途Node.js使いますかね・・・。 ↩
-
あと、JSON.stringify/parseはJSエンジンネイティブ実装だし、最適化されまくってるので速さで勝てません。憎いです。 ↩