中年engineerの独り言 - crumbjp

LinuxとApacheの憂鬱

Javascriptのコンテナ性能比較

ちょっとスピード狂をやる必要があって色々調査した。

こういう選択は用途によってコンテナのアルゴリズムが決まる。 しかしJS標準ではC/C++STLのようなアルゴリズムの選択肢が無くNPMに頼ることになるが実装まで読まないと何をしているのか解らないモジュールや効率的に実装されているかどうかも解らない。 一々性能試験をしなきゃならず面倒だが、やらない訳にもいかない。。

想定状況

  • 数値データ
  • 値は重複する
  • insert / delete が頻繁に発生する
  • 指定値による参照が大量に発生する
  • 範囲検索が起きる
  • 長時間安定稼働する必要がある

分岐検索とイテレータブルを両立し、かつ効率的な挿入削除ができること。が必要。

アルゴリズムとしてはバイナリーツリーとデータ量によってはソート済み配列が候補になる。

AVLやRedBlackはinsert / deleteがランダムに入る状況では採用したくないので除外。

テストコード

GitHub - crumbjp/test_js_containers

テスト対象モジュール

期待しているのは、★ だけ。あとはネタ枠。ほんの少しだけ掘り出し物を期待。

標準Array(比較対象)

ソートせずにpush。線形探索。spliceで削除。

★ 標準Arrayをソート済みに保つ

ソート済みを保ちlodashのsortedIndexByで2分探索。 insert / delete はsplice。位置の特定までは早いが、spliceのindex付け替えコストが高い。

★ @tylerbu/sorted-btree-es

www.npmjs.com

sorted-btree のクローン。 ウクライナの問題もあって、本家がCommonJS形式で読み込めなくなっている状態に対処している。’

そのままでは、値の重複が許されないため、データ構造側で少し工夫をしている

taffydb

taffydb - npm

GPT4oが教えてくれた。古すぎる・・・ インデックスを指定しなくともクエリーを最適化してくれるらしい。(そんな馬鹿な)

★ LokiJS

www.npmjs.com

コンテナというよりインメモリーデータベース。有名どころだが、もう何年も更新がない。

柔軟にインデックスを貼れるので便利。

ただクエリーが思ったように動かない事があり、複雑が故にその解析も大変で、多く個人的には好きではない。

nedb(比較対象)

nedb - npm

昔気になっていたもの。インメモリーでも使えるデータベース。

TingoDB

tingodb - npm

昔気になっていたもの。MongoDB互換のクエリーを謳ったインメモリーデータベース。

この頃流行ったスタイルだなぁ・・・と懐かしくなる。

結果

M3 Mackbook pro で計測

コンテナに保持しているデータ数毎、各処理を1000回試行した際の処理時間(ms) ただしrange処理は1回だけ試行

insert

class 1067 5257 10744 53009 105667 317043 528166 739974 1056478
TestArray 0 0 1 0 1 14 1 1 2
TestLoki 1 1 3 6 11 88 153 206 325
TestNedb 23 21 20 25 24 26 30 30 27
TestTingodb 8 5 4 8 4 6 13 5 12
TestBTree 1 1 1 0 2 1 1 1 2
TestSortedArray 1 0 0 15 19 404 709 976 1707
TestTaffy 6 5 6 6 6 5 5 5 5

SortedArrayはデータが大きくなるとspliceの処理が如実に効いてくる。

Lokiはインデックス構築のコストが高い。

adaptiveBinaryIndices: false を指定するとindexの処理を遅延実行(その後の最初のfind時)してくれるためbulkinsertで使えると思いきや、indexへの挿入ではなく完全再構築になり余計遅くなる。

findOne

class 1067 5257 10744 53009 105667 317043 528166 739974 1056478
TestArray 9 28 53 237 446 1204 1730 2326 2815
TestLoki 3 1 2 2 4 3 3 2 4
TestNedb 44 42 42 50 42 41 42 46 49
TestTingodb 63 58 59 64 59 58 72 73 75
TestBTree 2 1 0 1 1 1 1 1 1
TestSortedArray 1 0 1 0 1 1 0 0 2
TestTaffy 234 727 1337 6192 12202 36712 61332 87991 127708

単純に2分探索を実現できているものとそうでないものに分かれる。

線形探索より遅いtaffyはどうした(笑)。GPTもこんなものを勧めるんじゃないよ。

rangeGet

class 1081 5314 10585 52874 105466 317402 528743 739463 1057958
TestArray 1 0 0 1 2 5 14 10 16
TestLoki 0 0 0 1 1 1 1 6 6
TestNedb 2 1 1 3 3 12 20 26 36
TestTingodb 12 12 14 35 56 148 250 325 613
TestBTree 0 0 0 0 0 1 1 2 2
TestSortedArray 0 0 0 0 0 0 0 0 0
TestTaffy 1 1 1 9 15 43 73 119 142

線形探索が侮れない速さ。下手にインデックスを追う処理より全部舐めた方が早いケースも多々あるということ。

delete

class 1081 5314 10585 52874 105466 317402 528743 739463 1057958
TestArray 6 27 35 255 477 1006 2534 2264 3012
TestLoki 7 9 13 40 108 282 502 655 896
TestNedb 45 43 48 46 48 50 51 58 56
TestTingodb 117 114 115 164 140 118 129 126 131
TestBTree 2 2 2 2 2 2 2 3 3
TestSortedArray 1 1 1 24 51 411 664 980 1502
TestTaffy 455 1675 3293 17075 35283 102415 12309541 29964609 17357942

やはりspliceのコストが目立つ

まとめ

  • sorted-btreeの実装は期待通りだった。
  • 期待していなかったnedbは完成度が高かった。もっと複雑な用途でインデックスを複数貼りたいようなケースでは検討できる
  • LokiJSはinsert / delete に弱く、クエリーは早い。データ量が少なかったり、更新頻度が低いならば検討できる
  • やはりNPMモジュールは期待通りに動かないので、検証活動は必須