JavaScript Map vs Object (パフォーマンス編)

JavaScript, Map, Object
2022-03-26

前記事の続き

JavaScript Map vs Object(実装編) | kyohei's blog
JavaScriptでは単純な連想配列(ハッシュマップ)を実装する場合、MapまたはObjectを利用することが多いと思う、使い方がよく似ているためどちらを選択していいか迷うときがある、今回はその違い、パフォーマンス、使い所をまとめてみた。 JavaScriptは元来(ES5までは)、プリミティブな型以外の"データの集合"を表す場合、すべてObjectとなっており、ArrayやFunctionも実はObjectである。 ES6において、classやSetなどデータの集合を表すための表現がいくつか追加され、その中の一つにKey-Valueのデータを扱うための新しいアプローチとしてMapも追加された。 とはいえ、JSON.parseのように、純粋なObject型を使うケースはまだまだ存在し、どのようなタイミングでMapを利用するかが不明瞭だったので、それを少し調べてみた。 Mapの一番の特徴はKeyにString以外の値を設定できるところかと思う。 また、Object型のインスタンスはObjectのprototypeを参照することになるので、valueOfやtoStringなどのキーをデフォルトで持つことになる。また、最近は見なくなったけど、Object.prototypeを拡張している場合そのプロパティも参照できてしまうことになる。(そのため、hasOwnPropertyを挟むとか昔はよくあった) Mapではこのようなことが起きない。 Mapではコンストラクタの引数にkey-valeu配列の配列を渡すことでデフォルトの値を定義することができる。 Objectでは宣言時のオブジェクトリテラル {} 内に値を定義する Mapでは set メソッドを利用して値を登録する。keyが重複した場合は当然値が上書きされる。 Objectでは . でkeyを直接指定、またはハイフンを含むキーや全角文字の場合クォートを付けた文字列リテラルまたは文字列の変数を利用して、設定する方法がある。 Mapにおける値の取得は get メソッドを利用する。keyが存在しない場合は undefinde が返る。 Objectは設定時と同様で . でkeyを直接指定、または [] 内に文字列を入れて呼び出す方法がある。キーが存在しない場合は undefined が返る。 Mapでは delete メソッドを利用して指定のキー及び値を削除することができる。削除が成功すると、 true を返し、keyが存在しない場合は、 false を返す。 また、map内のすべての値を削除する clear メソッドも用意されている。 Objectでは、 delete 演算子を用いてキー及び値を削除する。キーが自分自身のnon-configurableのプロパティであった場合、 strict モードでなければ false を返し、それ以外はtrueを返す。つまりキーが存在しない場合でもtrueを返す。 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cant_delete Mapでキーの存在を確認する場合は has メソッドを利用します。キーが存在する場合はtrue、存在しない場合はfalseを返す。 Objectでは hasOwnProperty メソッドで自身のオブジェクトにあるキーかをチェックすることができる。 Mapでは size メソッドが用意されており、簡単に要素数を取得できる。値が0であるかを判定する事によってemptyかのチェックもできる。 Objectでは Object.entries 、 Object.keys 、 Object.values にて配列に変換し、その要素数を取得するというのが一般的なやり方である。一度配列に変換するので、コストがかかるという印象。 余談だが、keysよりもvaluesの方がChrome,Firefoxでは パフォーマンスが良い 。 Map, Objectともに走査の方法はいくつか存在する。 Mapはイテレータブルなオブジェクトなので、 for ..

パフォーマンス

結局、どちらも同じようなことができることはわかったので、パフォーマンス的にどちらを利用するかを検証してみます。

Perflikを利用して、各処理のブラウザでのパフォーマンスを図ってみた。

(macOS Monterey)
Chrome Version 99
Safari Version 15.4
Firefox Version 98
(windows 10)
Edge Version 99

値の登録

m.set(x, y) vs o[x] = y

いずれのブラウザでも、Objectの方が速い。

Perflink
JavaScript performance benchmarks you can share via URL.
An image from Notion

Chrome(V8)

An image from Notion

Safari(JSC)

An image from Notion

Firefox(SM)

An image from Notion

Edge(Chakra)

値の取得

m.get(x) vs o[x]

こちらもいずれのブラウザでもObjectの方がやや速い。

(このテストはランダム性があるので何回行ってみた)

Perflink
JavaScript performance benchmarks you can share via URL.
An image from Notion

Chrome(V8)

An image from Notion

Safari(JSC)

An image from Notion

Firefox(SM)

An image from Notion

Edge(Chakra)

値の削除

m.delete(x) vs delete o[x]

これは各ブラウザでばらつきがでた。Chrome,Edgeだと delete メソッドの方が速いが、Safariだとdeleteオペレータの方が速い。Firefoxはほぼ同じくらいだがmapの方がやや速い。

Perflink
JavaScript performance benchmarks you can share via URL.
An image from Notion

Chrome(V8)

An image from Notion

Safari(JSC)

An image from Notion

Firefox(SM)

An image from Notion

Edge(Chakra)

走査

これはいくつかの操作のパターンを試してみた。

// 1. Map: for of パターン
for(const [k, v] of m){ sum += v; }

// 2. Map: forEachメソッドパターン
m.forEach((v, k) => { sum += v; });

// 3. Map: 配列に変換し、ArrayのforEachを使うパターン
[...m].forEach(([k, v]) => { sum +=v });

// 4. Map: 配列に変換し、for文を使うパターン
let sum = 0;
const _m = [...m];
const len = _m.length;
for(let i = 0;i < len; i++){ sum += _m[i][1]; }

// 5. Object: Object.entriesでfor..ofを使うパターン
for(const [k, v] of Object.entries(o)){ sum += v }

// 6. Object: Object.values で for..ofを使うパターン
for(const v of Object.values(o)){ sum += v }

// 7. Object: Object.keys で for..ofを使うパターン
for(const k of Object.keys(o)){ sum += k }

// 8. Object: Object.values で ArrayのforEachを使うパターン
Object.values(o).forEach((v) => sum += v )

// 9. Object: Object.values で for文を使うパターン
const vas = Object.values(o);
const len = vas.length;
for(let i = 0; i < len; i++){ sum += vas[i] }

// 10. Object: for..inのパターン
for(const k in o){ if(o.hasOwnProperty(k)){sum += o[k]} }
  • ChormeとFirefox,Edgeはほぼ同じような結果になり、Safariは結構違う結果になった。
  • valueのみを利用したい場合は 9 の Object.values で変換し、昔ながらのfor文を使って走査するのが速い。
  • 次点で for(const v of Object.values(o))
  • mapではネイティブのメソッドである forEach がどのブラウザでも速かった。
  • Safariでは mapのforEachが圧倒的に早く、他のmap, objectの走査はどれも遅かった。
    • mapのforEach以外だと、Objectの for..in が次点で速かった。

結論としては、走査の書き方によって処理速度が顕著に変わる、mapの場合は forEach 一択である。Objectの場合はvalue限定であれば、通常のfor文を利用するが一番速いが、バランスを取って for(const v of Object.values(o)){} でもよいと思われる。

Perflinkリンク

An image from Notion

Chrome(V8)

An image from Notion

Safari: mapのforEachだけが異常に速く、それ以外はだいぶ遅い

An image from Notion

SafariでmapのforEachを除いた場合は、for..inが速かった

An image from Notion

Firefox(SM): おおよそChromeと同じ

An image from Notion

Edge(Chakra): こちらもChrome, Firefoxとおおよそ同じ

まとめ

ObjectとMapは同じ使い方ができるが、Mapを使う場合の指標としては以下の点が上げられる

  • キーに文字列(またはSymbol)以外を指定したい場合はMap一択となる
  • プロパティの追加、削除が頻発する場合は、Mapの方がパフォーマンスが上がる。
  • 走査する場合、 Object.keysObject.values を使わずに走査できるので、可読性が上がり、処理速度も速い。
  • Objectの要素数や空の判定も、もともと効率的な書き方ができなかったので、 sizeメソッドによる恩恵も大きいと思う

JavaScriptはもともと辞書型を定義するものがObjectしかなかったので、仕方なくそれを使っていた面もあるので、機会(と要件)が合うのであれば積極的にMapも使って行きたい所である。

Written by Kyohei Tsukuda who lives and works in Tokyo 🇯🇵 , building useful things 🔧.