web-dev-qa-db-ja.com

パフォーマンス:Immutable.jsマップvsリストvsプレーンJS

質問

ベンチマークに何か問題がありますか? Immutable.jsのfind()をarray.find()の8倍遅くするにはどうすればよいですか?

私はImmutable.List内でImmutable.Mapを使用しているため、完全に公平ではありません。しかし、私にとってこれは実世界の例です。 Immutable.jsを使用する場合、それは不変性を保護し、いくつかの側面(構造的な共有が行われるようになる)でパフォーマンスを得るためです。オブジェクトのルートでのみImmutable.jsを使用しても意味がありません。

以下のベンチマークは、実際には 別の質問 (同様に)からのものです。結果に驚いたので、まっすぐにするために個別に投稿する必要がありました。ベンチマークで何か間違ったことをしたことがありますか、それともパフォーマンスの差は本当に大きいですか?

背景

アプリ内のデータの一部は、アプリのメタデータと見なすことができます。元のデータはサーバーのデータベースに存在します。メタデータの更新は頻繁に行われません。アプリは起動時に更新されたメタデータを確認します。

どこでもImmutable.jsを使用していますが、メタデータのプレーンjsに戻ります。この種のデータのための派手な構造共有の必要はありません。

テストは、コレクション内のキーによって値を見つけることです

  • 10アイテムのコレクション

  • 100万回の値を見つける

  • Mac mini Core i7 2.6

結果:

  • 強制キーを持つプレーンJSオブジェクト:8 ms

  • Find()を使用したプレーンJS配列:127 ms

  • Immutable.Mapと数字キー:185 ms

  • Find()を使用した不変のリスト:972 ms!! 私は困惑しています

React Nativeを使用しているので、60 fpsを達成したい場合は常に16ミリ秒の制限に注意する必要があります。ベンチマーク値は線形ではないようです。 100回の検索には、Mapで1ミリ秒、Listで2ミリ秒かかります。

テストコード

let Immutable = require('immutable');

let mapTest = Immutable.Map()
  .set(1, Immutable.Map({value: 'one'}))
  .set(2, Immutable.Map({value: 'two'}))
  .set(3, Immutable.Map({value: 'three'}))
  .set(4, Immutable.Map({value: 'four'}))
  .set(5, Immutable.Map({value: 'five'}))
  .set(6, Immutable.Map({value: 'six'}))
  .set(7, Immutable.Map({value: 'seven'}))
  .set(8, Immutable.Map({value: 'eight'}))
  .set(9, Immutable.Map({value: 'nine'}))
  .set(10, Immutable.Map({value: 'ten'}));

let listTest = Immutable.fromJS([
  {key: 1,  value: 'one'},
  {key: 2,  value: 'two'},
  {key: 3,  value: 'three'},
  {key: 4,  value: 'four'},
  {key: 5,  value: 'five'},
  {key: 6,  value: 'six'},
  {key: 7,  value: 'seven'},
  {key: 8,  value: 'eight'},
  {key: 9,  value: 'nine'},
  {key: 10, value: 'ten'}
])

let objTest = {
  1:  {value: 'one'},
  2:  {value: 'two'},
  3:  {value: 'three'},
  4:  {value: 'four'},
  5:  {value: 'five'},
  6:  {value: 'six'},
  7:  {value: 'seven'},
  8:  {value: 'eight'},
  9:  {value: 'nine'},
  10: {value: 'ten'}
};

let arrayTest = [
  {key: 1,  value: 'one'},
  {key: 2,  value: 'two'},
  {key: 3,  value: 'three'},
  {key: 4,  value: 'four'},
  {key: 5,  value: 'five'},
  {key: 6,  value: 'six'},
  {key: 7,  value: 'seven'},
  {key: 8,  value: 'eight'},
  {key: 9,  value: 'nine'},
  {key: 10, value: 'ten'}
];

const runs = 1e6;
let i;
let key;
let hrStart;

console.log(' ')
console.log('mapTest -----------------------------')
key = 1;
hrstart = process.hrtime();
for(i=0; i<runs; i++) {
  let result = mapTest.getIn([key, 'value'] )
  key = (key >= 10) ? 1 : key + 1;
}
hrend = process.hrtime(hrstart);
console.info("Execution time (hr): %dms", hrend[0] * 1000 + hrend[1]/1000000);


console.log(' ')
console.log('listTest -----------------------------')
key = 1;
hrstart = process.hrtime();
for(i=0; i<runs; i++) {
  let result = listTest
    .find(item => item.get('key') === key)
    .get('value');
  key = (key >= 10) ? 1 : key + 1;
}
hrend = process.hrtime(hrstart);
console.info("Execution time (hr): %dms", hrend[0] * 1000 + hrend[1]/1000000);

console.log(' ')
console.log('arrayTest -----------------------------')
key = 1;
hrstart = process.hrtime();
for(i=0; i<runs; i++) {
  let result = arrayTest
    .find(item => item.key === key)
    .value

  key = (key >= 10) ? 1 : key + 1;
}
hrend = process.hrtime(hrstart);
console.info("Execution time (hr): %dms", hrend[0] * 1000 + hrend[1]/1000000);


console.log(' ')
console.log('objTest -----------------------------')
key = 1;
hrstart = process.hrtime();
for(i=0; i<runs; i++) {
  let result = objTest[key].value
  key = (key >= 10) ? 1 : key + 1;
}
hrend = process.hrtime(hrstart);
console.info("Execution time (hr): %dms", hrend[0] * 1000 + hrend[1]/1000000);
28
Michael

簡単な答えは、Immutable.jsで使用されるデータ構造の表現は、ネイティブのJS配列と比較して、Listの要素を反復処理するために多くの追加のオーバーヘッドを必要とすることです。

Immutable.List.findおよびArray.findのベンチマーク

ベンチマークは優れていますが、ネストされたマップを削除することで問題を少し簡素化できます。現実的な問題のパフォーマンスを考慮するのは正しいことですが、パフォーマンスの違いを理解することは、問題をできるだけ単純化するのに役立ちます。また、ベンチマークでは、さまざまな入力サイズでパフォーマンスがどのように変化するかを検討するのにも役立ちます。たとえば、Immutable.jsでは、_List.prototype.find_が実装され、初期呼び出しとセットアップにはしばらく時間がかかりますが、その後のListの反復処理はネイティブJS配列と同様に実行されます。この場合、ネイティブのJS配列とImmutable.jsリストのパフォーマンスの違いは、入力長が長いと小さくなります(これは事実ではありません)。

また、ネイティブJS配列用の独自の検索関数_Array.prototype.ourFind_を作成して、ネイティブ_Array.prototype.find_と比較し、違いが一部はJS関数自体のパフォーマンスと構築された関数のパフォーマンスに起因するかどうかを判断します-実装に。

_Array.prototype.ourFind = function(predicate) {
  for (let i = 0; i < this.length; i++) {
    if (predicate(this[i])) return this[i];
  }
}

function arrayRange(len) {
  return new Array(len).fill(null).map((_, i) => i);
}

function immutListRange(len) {
  return Immutable.fromJS(arrayRange(len));
}

function timeFind(coll, find, iters) {
  let startTime = performance.now();
  for (let i = 0; i < iters; i++) {
    let searchVal = i % coll.length,
      result = find.call(coll, item => item === searchVal);
  }
  return Math.floor(performance.now() - startTime);
}

const MIN_LEN = 10,
  MAX_LEN = 1e4,
  ITERS = 1e5;

console.log('\t\tArray.find\tArray.ourFind\tList.find');
for (let len = MIN_LEN; len <= MAX_LEN; len *= 10) {
  console.log(`${len}\t\t\t` +
    `${timeFind(arrayRange(len), Array.prototype.find, ITERS)}\t\t\t` +
    `${timeFind(arrayRange(len), Array.prototype.ourFind, ITERS)}\t\t\t` +
    `${timeFind(immutListRange(len), Immutable.List.prototype.find, ITERS)}`)
}_
_<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.js"></script>_

Chromeでは、次のようになります:

_Length .    Array.find  Array.ourFind   List.find
10          28          13              96
100         60          44              342
1000        549         342             3016
10000       5533        3142            36423
_

FirefoxとSafariでもほぼ同じ結果が得られました。注意すべきいくつかのポイント:

  1. _List.find_と_Array.find_の違いは、ネイティブ(つまり、インタープリター組み込み)実装とJS実装の違いだけではありません。なぜなら、_Array.ourFind_のJS実装は、 _Array.find_。
  2. すべての実装はO(n)時間で動作します(つまり、実行時間は入力長に対して線形です)。これは予想されます。述語がtrueを返すコレクション要素が見つかるまで、コレクション要素。
  3. _Immutable.List.find_は、_Array.find_よりも6倍遅く、ベンチマークの結果と一致します。

Immutable.Listデータ表現

_Immutable.List.find_が非常に遅い理由を理解するには、最初に_Immutable.List_がリストの内容を表す方法を考慮する必要があります。

これを行う簡単な方法は、_Immutable.List_を生成し、コンソールで調べることです:

_console.log(immutListRange(1000));  // immutListRange defined above
_

したがって、本質的には、_Immutable.List_は内容を32の分岐係数を持つツリーとして表しているように見えます。

次に、この方法で表されたデータに対して検索操作を実行するために必要なことを検討します。ルートノードから開始し、ツリーを最初のリーフノード(実際のデータを含む配列を含む)まで下にトラバースし、リーフのコンテンツを反復処理する必要があります。要素が見つからない場合は、次のリーフノードに移動してその配列を検索する必要があります。単一の配列を単に検索するよりも複雑な操作であり、実行にはオーバーヘッドが必要です。

職場でのImmutable.List.findの監視

_Immutable.List.find_が行う作業を評価するための優れた方法は、選択したデバッガーにブレークポイントを設定し、操作をステップスルーすることです。 _Immutable.List.Find_は、単一の配列をループするだけの単純な操作ではないことがわかります。

追加コメント

Immutable.jsのデータのツリー表現は、おそらく他の操作を高速化しますが、findなどの一部の機能ではパフォーマンスが低下します。

補足として、ほとんどの場合、不変のデータ構造を使用する選択はパフォーマンスの考慮事項に左右されるとは思いません。不変のデータ構造は、可変のデータ構造よりも優れたパフォーマンスを発揮する場合があります(そして、確かに不変のデータ構造により、並列計算の複雑さが軽減され、パフォーマンスが大幅に向上します)。むしろ、不変性の選択は、ほとんどの場合、設計上の考慮事項によって決まります。不変のデータ構造を使用すると、プログラム設計がより堅牢になり、長期的には開発者の生産性が向上します。

22
cjg

JSエンジンは、「ホット」操作の最適化に非常に優れています-繰り返しが多く、可能な限り単純な操作(たとえば TurboFan in V8 )。プレーンJSオブジェクトと配列関数は、alwaysImmutable.jsのようなライブラリに勝ちます。ここで List 呼び出し Collection 呼び出し Seq 呼び出し Operations (など)、特にアクションは何度も繰り返されます。

Immutable.jsは、純粋なパフォーマンスではなく、使用の利便性と、変更可能なJSコレクションの厄介さの多くを回避するように設計されているようです。

100万個ある場合は、低レベルのJSオブジェクトまたは配列(またはパフォーマンスが重要な場合はWebアセンブリ)を使用します。数千ものものがあり、needがフレームを落とさないことを確実にする必要がある場合は、プレーンJSがまだ道です。ただし、これらは特殊なケースです-ほとんどのユースケースでは、Immutable.jsの利便性は速度を下げる価値があります。

11
Keith

ベンチマークは、Immutableが提供しなければならないすべてのデータ型を考慮していません。不変には、実際にはプレーンオブジェクト/配列にはないいくつかの機能があります: OrderedSet および OrderedMap インデックス付き配列/リストとオブジェクト/マップのようなキーベースの構造の両方の利点があります。

以下は、@ Keithの適切に実行されたテストの適合バージョンです。これは、特に大きなデータセットの場合、実際にArray.findよりも速くなることを示しています。

もちろん、これには多少のコストもかかります。

  • Set/Mapは重複を許可しません(ただし、オブジェクトとは異なります)。
  • 舞台裏では、順序付けられたバリアントはマップ/セットとリストを組み合わせているため、より多くのメモリを消費します。

OrderedSetは非順序セットよりも高価であり、より多くのメモリを消費する可能性があることに注意してください。 OrderedSet#addは償却O(log32 N)ですが、安定していません。

function arrayFind(coll, searchVal) {
  return coll.find(item => item === searchVal);
}

function immutableSetGet(coll, searchVal) {
  return coll.get(searchVal);
}

function arrayRange(len) {
  return new Array(len).fill(null).map((_, i) => i);
}

function immutOrderedSetRange(len) {
  return Immutable.OrderedSet(arrayRange(len));
}

function timeFind(what, coll, find, iters) {
  let startTime = performance.now();
  let size = coll.length || coll.size;
  for (let i = 0; i < iters; i++) {
    let searchVal = i % size,
      result = find(coll, searchVal);
  }
  return Math.floor(performance.now() - startTime);
}

const MIN_LEN = 100,
  MAX_LEN = 1e4,
  ITERS = 50000;

console.log('\t\t\tArray.find\tOrderedSet.find');
for (let len = MIN_LEN; len <= MAX_LEN; len *= 10) {
  console.log(`${len}\t\t\t` +
    `${timeFind('find', arrayRange(len), arrayFind, ITERS)}\t\t` +
    `${timeFind('set', immutOrderedSetRange(len), immutableSetGet, ITERS)}`)
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.js"></script>
1
dube