Node.jsのforEachとmapの速度を比較してみた
背景
仕事でNode.jsで書かれたコードのPull Requestを見ていて、forEachで新しい配列を生成しているのを見かけました。
そこで、forEachとmapのどちらが速いか少し気になったのでGoogleで調べてみましたが、調べ方が悪かったのかNode.jsでの比較結果を見つけられなかったので、自分で比較してみました。
比較方法
数値1が100万個入った配列をもとに、文字列が100万個入った別の配列を生成する処理を、forEachとmapのそれぞれで1000回実行。 (コード全文は#今回使ったコード参照)
- forEachの処理
const arr2 = []; arr1.forEach((elem) => { arr2.push(`${elem} hoge`); });
- mapの処理
const arr2 = arr1.map((elem) => {`${elem} hoge`});
環境
- Node.js:v8.9.1 LTS
- MacOS High Sierra 10.13.2
- プロセッサ名:Intel Core i7 2.9 GHz (4コア)
- メモリ:16 GB 2133 MHz LPDDR3
結果
forEach | map | |
---|---|---|
max | 270.58ms | 330.12ms |
min | 165.22ms | 250.97ms |
average | 210.99ms | 278.82ms |
おおよそforEachで処理する方が20%程度速いという結果になりました。
forEachとmapでは使いどころが違ってくるので、どちらが良いか悩む場面は少ないと思いますが、どっちでもいいような時はforEachの方が良いかもしれません。
今回使ったコード
ElasticSearchで親-子-孫の3世代ドキュメントを作って、子や孫の内容で検索する
前職の時、AWSのElasticSearchService (Ver5.3) を使っていましたが、日本語どころか英語でもあまり情報がなかったりするので苦労しました。
RDBでよくある、テーブル結合して検索ということがしたかったのですが、手間取ったので方法を残しておきます。
ElasticSearchで3世代ドキュメント構造を持ったindexを作る
たとえば、「商品カテゴリ - 商品 - 商品のレビュアー」みたいに、それぞれのデータに1:nの関係性を持たせたい場合、RDBなら外部キーを使ってJOINすればOKです。
ElasticSearchで、そういった構造を表現したい場合、_type
を使います。
親の方から1,2,3世代とするとき、1世代目、2世代目、3世代目に該当する_type
の名前を決めて、インデックスを設計します。
上の例の場合、名前をcategory
、product
、reviewer
とするなら、以下のようなmappingのJSONをインデックスに適用しておきます。
PUT /market
{ "mappings": { "category": {}, "product": { "_parent": { "type": "category" } }, "reviewer": { "_parent": { "type": "product" } } } }
RDBで各テーブルのレコードに外部キーを持たせるのと違う点は、「あくまで1つのindexの中にフラットなデータとして入っている」ということです。
要は_id
の重複などを気にしないといけません。
3世代ドキュメント構造を持ったindexにデータを放り込む
- 2世代目のドキュメントを放り込む時、
_parent: 1世代目の_id
(例:_parent: "c1"
)を追加で渡します。 - 3世代目のドキュメントを放り込む時、
_parent: 2世代目の_id, _routing: 1世代目の_id
(例:_parent: "p1", _routing: "c1"
)を追加で渡します。
これを_bulkAPIに渡すJSONとして表現するなら
{ "index": { "_index": "market", "_type": "category", "_id": "c1" } } {"category_name": "clock" } { "index": { "_index": "market", "_type": "product", "parent": "c1", "_id": "p1" } } {"product_name": "clock1" } { "index": { "_index": "market", "_type": "reviewer", "parent": "p1", "routing": "c1", "_id": "r1" } } {"reviewer": "hoge" }
みたいになります。
こうすることで、index内が、図で表すと以下のようなドキュメント構造になります。
子ドキュメントで検索し、親・孫ドキュメントを含めた一連のドキュメントを取得する
これがあまり情報がなかったのですが、以下のようなクエリを書けば子ドキュメントのデータで検索した上で、3世代分のドキュメントを結合して一気に返してもらうことができます。
{ "query": { "has_child" : { "type": "product", "query": { "bool": { "must": [ { "term": {"product_id": "123"} }, { "has_child": { "type": "reviewer", "inner_hits": {}, "query": {"match_all": {} } } } ] } }, "inner_hits": {} } } }
bool
クエリで、2世代目への検索クエリ(term
)と、3世代目の全件取得(has_child
.query
.match_all
)をまとめるのがポイントです。
※ もちろん、3世代目で更に絞り込みをしたければ、match_all
の代わりに別のクエリを使います。
それと、inner_hitsを書くことで、2、3世代目のドキュメント一式を取得できます。 書き忘れると1世代目のドキュメントしか返ってきません。
注意点
公式見解として、今後は1つのindexに対して1つのtypeが推奨されています。
また、親子関係をもたせたいときは5.6から追加されたjoin
フィールドを使って欲しいとのこと。
今後のロードマップによると、バージョン8.xあたりでtypeが事実上消滅するようです。(まだまだ先の話でしょうが。。)
AWSだと、12/6時点でもまだ5.5までしか選択できないですが、5.6以降がサポートされたら単一type & join
フィールドを使う方向で設計した方が良さそうですね。
Electronでパッケージを作ろうとした時に「May not delete」というエラーが出る
electron-packagerでElectronでパッケージを作ろうとした時、以下のエラーが発生しました。
$ electron-packager . sample --platform=darwin --arch=x64 --overwrite --out build" npm ERR! May not delete: /private/var/folders/q3/hg9ht5vx5rjddgghnk8z9w480000gn/T/electron-packager/darwin-x64/sample-darwin-x64/Electron.app/Contents/Resources/app/node_modules/.bin npm ERR! A complete log of this run can be found in: npm ERR! /Users/hogehoge/.npm/_logs/2017-11-14T10_35_53_001Z-debug.log
npmのバージョンが問題だったらしく、5.3だとダメでした。
内部で実行しているnpm prune --production
がやらかしている模様。
なお、最新の5.5.1ならOKでした。