web-dev-qa-db-ja.com

JavaScriptのオブジェクト/配列のパフォーマンスはどうですか? (特にGoogle V8の場合)

JavaScriptの配列とオブジェクト(特にGoogle V8)に関連するパフォーマンスは、文書化するのに非常に興味深いでしょう。このトピックに関する包括的な記事は、インターネット上のどこにもありません。

一部のオブジェクトは、基礎となるデータ構造としてクラスを使用することを理解しています。プロパティがたくさんある場合、それは時々ハッシュテーブルとして扱われますか?

また、配列がC++配列のように扱われることもあることを理解しています(つまり、高速ランダムインデックス、低速削除、サイズ変更)。また、オブジェクトのように扱われる場合もあります(高速インデックス作成、高速挿入/削除、より多くのメモリ)。また、リンクリストとして保存される場合もあります(つまり、ランダムなインデックス作成が遅く、開始/終了時に削除/挿入が高速になります)

JavaScriptでの配列/オブジェクトの取得と操作の正確なパフォーマンスは?(特にGoogle V8の場合)

より具体的には、パフォーマンスへの影響:

  • オブジェクトにプロパティを追加する
  • オブジェクトからプロパティを削除する
  • オブジェクトのプロパティのインデックス作成
  • 配列にアイテムを追加する
  • 配列からアイテムを削除する
  • 配列内のアイテムのインデックス作成
  • Array.pop()を呼び出す
  • Array.Push()を呼び出す
  • Array.shift()を呼び出す
  • Array.unshift()を呼び出す
  • Array.slice()を呼び出す

さらに詳細な記事やリンクも歓迎します。 :)

編集: JavaScriptの配列とオブジェクトが内部でどのように機能するのか、本当に疑問に思っています。また、V8エンジンは、どのcontextで別のデータ構造への「切り替え」を「認識」していますか?

たとえば、配列を作成するとします...

var arr = [];
arr[10000000] = 20;
arr.Push(21);

ここで実際に何が起こっているのですか?

または...これはどうですか... ???

var arr = [];
//Add lots of items
for(var i = 0; i < 1000000; i++)
    arr[i] = Math.random();
//Now I use it like a queue...
for(var i = 0; i < arr.length; i++)
{
    var item = arr[i].shift();
    //Do something with item...
}

従来のアレイの場合、パフォーマンスはひどいものになります。一方、LinkedListが使用された場合は、それほど悪くありません。

103
BMiner

UPDATE: JSPrefが現在ダウンしていることに注意してください

(テストケースのコピーを保存し、JSPrefが修正されると回答が更新されます/後継者が見つかった場合)


うーん...多分答えに行き過ぎ...しかし、私は テストスイート、これらの問題(およびその他)を正確に調査するためにアーカイブコピー )を作成しました。

その意味で、この50以上のテストケーステスターでパフォーマンスの問題を確認できます(時間がかかります)。

また、その名前が示すように、DOM構造のネイティブリンクリストの性質を使用する使用法を検討します。

(現在ダウン中、再構築中)これに関する私のブログの詳細

概要は次のとおりです

  • V8アレイは高速で、非常に高速です
  • 配列のプッシュ/ポップ/シフトは、同等のオブジェクトよりも約20倍以上高速です。
  • 驚くべきことにArray.shift()は配列ポップよりも約6倍高速ですが、オブジェクト属性の削除よりも約100倍高速です。
  • 面白いことに、Array.Push( data );は_Array[nextIndex] = data_よりも20(動的配列)から10(固定配列)倍だけ高速です。
  • Array.unshift(data)は予想どおり遅く、新しいプロパティの追加よりも約5倍遅いです。
  • 値_array[index] = null_をNULLにすることは、配列内で_delete array[index]_(未定義)を削除するよりも〜approx 4x ++により速くなります。
  • 驚くべきことに、オブジェクトの値をヌルにすることは_obj[attr] = null_〜属性を削除するよりも約2倍遅い_delete obj[attr]_
  • 当然のことながら、中間配列Array.splice(index,0,data)は非常に低速です。
  • 驚いたことに、Array.splice(index,1,data)は最適化され(長さの変更なし)、スプライスArray.splice(index,0,data)の100倍高速です。
  • 当然のことながら、divLinkedListはdll.splice(index,1)の削除(テストシステムを壊した部分)を除いて、すべてのセクターの配列よりも劣っています。
  • 最大のサプライズ[jjrvが指摘したように]、V8配列の書き込みはV8読み取り= Oよりもわずかに高速です

注:これらのメトリックは、v8が「完全に最適化」しない大規模な配列/オブジェクトにのみ適用されます。配列/オブジェクトのサイズが任意のサイズ(24?)未満の場合、非常に孤立した最適化されたパフォーマンスのケースがあります。詳細は、複数のgoogle IO動画。

注2:これらの素晴らしいパフォーマンスの結果は、ブラウザ間、特に_*cough*_ IEで共有されません。また、テストは巨大であるため、結果をまだ完全に分析および評価することはできません:=)で編集してください

更新された注(2012年12月):Googleの担当者は、chrome自身の内部動作を説明する動画をYouTubeに掲載しています。リンクリスト配列から固定配列への切り替えなど)、およびそれらの最適化方法。詳細については、 GDC 2012:コンソールからChromeへ を参照してください。

更新された注(2013年2月):正確な時点でビデオリンクを提供するためのThx @badunk

注を更新(2016年6月):Thx @Benedikt、固定/動的配列の配列プッシュパフォーマンスの違いについて。

274
PicoCreator

JavaScriptの領域内にとどまる基本レベルでは、オブジェクトのプロパティははるかに複雑なエンティティです。列挙可能性、書き込み可能性、構成可能性が異なるセッター/ゲッターでプロパティを作成できます。配列内のアイテムは、この方法ではカスタマイズできません。存在するかしないかのいずれかです。基礎となるエンジンレベルでは、これにより、構造を表すメモリの編成に関してより多くの最適化が可能になります。

オブジェクト(辞書)から配列を識別するという点では、JSエンジンは常に2つの間の明示的な行を作成しています。 1つのように振る舞うが、他の機能を許可するセミフェイクの配列のようなオブジェクトを作成しようとする方法に関する記事が多数あるのはそのためです。この分離が存在する理由は、JSエンジン自体が2つを別々に保存するためです。

プロパティは配列オブジェクトに保存できますが、これはJavaScriptがすべてをオブジェクトにすることを主張する方法を示しています。配列内のインデックス付きの値は、基になる配列データを表す配列オブジェクトに設定することを決定したプロパティとは異なる方法で保存されます。

正当な配列オブジェクトを使用し、その配列を操作する標準的な方法のいずれかを使用するときはいつでも、基礎となる配列データにアクセスすることになります。特にV8では、これらは基本的にC++配列と同じであるため、これらの規則が適用されます。何らかの理由で、エンジンが自信を持って配列を決定できない配列を操作している場合は、はるかに不安定です。 V8の最近のバージョンでは、さらに作業する余地があります。たとえば、Array.prototypeがprototypeであるクラスを作成しても、さまざまなネイティブ配列操作メソッドに効率的にアクセスできます。しかし、これは最近の変更です。

配列操作に対する最近の変更への特定のリンクは、ここで役に立つかもしれません:

少し余分なものとして、V8のソースから直接Array PopとArray Pushがあり、どちらもJS自体に実装されています

function ArrayPop() {
  if (IS_NULL_OR_UNDEFINED(this) && !IS_UNDETECTABLE(this)) {
    throw MakeTypeError("called_on_null_or_undefined",
                        ["Array.prototype.pop"]);
  }

  var n = TO_UINT32(this.length);
  if (n == 0) {
    this.length = n;
    return;
  }
  n--;
  var value = this[n];
  this.length = n;
  delete this[n];
  return value;
}


function ArrayPush() {
  if (IS_NULL_OR_UNDEFINED(this) && !IS_UNDETECTABLE(this)) {
    throw MakeTypeError("called_on_null_or_undefined",
                        ["Array.prototype.Push"]);
  }

  var n = TO_UINT32(this.length);
  var m = %_ArgumentsLength();
  for (var i = 0; i < m; i++) {
    this[i+n] = %_Arguments(i);
  }
  this.length = n + m;
  return this.length;
}
5
user748221

Node.js 0.10(v8上に構築)で実行しているときに、ワークロードに対して過度に思えるCPU使用率が見られました。パフォーマンスの問題の1つは、配列内の文字列の存在をチェックしている関数に由来します。そこで、いくつかのテストを実行しました。

  • 90,822台のホストをロード
  • 設定の読み込みには0.087秒かかりました(配列)
  • 設定の読み込みには0.152秒かかりました(オブジェクト)

検証とプッシュを使用して、91kエントリを配列にロードする方が、obj [key] = valueを設定するよりも高速です。

次のテストでは、リスト内のすべてのホスト名を1回ルックアップしました(ルックアップ時間を平均するために91k回の反復)。

  • configの検索には87.56秒かかりました(配列)
  • 設定の検索には0.21秒かかりました(オブジェクト)

ここのアプリケーションは、Haraka(SMTPサーバー)であり、起動時に(および変更後に)Host_listを1回読み込み、その後、操作中にこの検索を何百万回も実行します。オブジェクトに切り替えると、パフォーマンスが大幅に向上しました。

0
Matt Simerson

配列の成長に関して実装がどのように動作するかという問題に対する調査で、既存の回答を補完したいと思います。「通常の」方法で実装すると、実装がコピーされる点で、まれに散在する低速プッシュで多くのクイックプッシュが表示されます1つのバッファからより大きなバッファへの配列の内部表現。

この効果は非常によくわかります。これはChromeからのものです。

16: 4ms
40: 8ms 2.5
76: 20ms 1.9
130: 31ms 1.7105263157894737
211: 14ms 1.623076923076923
332: 55ms 1.5734597156398105
514: 44ms 1.5481927710843373
787: 61ms 1.5311284046692606
1196: 138ms 1.5196950444726811
1810: 139ms 1.5133779264214047
2731: 299ms 1.5088397790055248
4112: 341ms 1.5056755767118273
6184: 681ms 1.5038910505836576
9292: 1324ms 1.5025873221216042

各Pushのプロファイルが作成されていても、出力には特定のしきい値を超える時間がかかるもののみが含まれます。各テストについて、高速プッシュを表していると思われるすべてのプッシュを除外するようにしきい値をカスタマイズしました。

したがって、最初の数字は挿入された要素を表し(最初の行は17番目の要素を表します)、2番目は所要時間(多くの配列でベンチマークが並列に行われます)、最後の値は前の行の番号の最初の番号。

実行時間が2ミリ秒未満の行はすべて、Chromeでは除外されます。

Chromeは1.5の累乗で配列サイズを大きくし、小さな配列を考慮するためにいくらかのオフセットを加えていることがわかります。

Firefoxの場合、2のべき乗です。

126: 284ms
254: 65ms 2.015873015873016
510: 28ms 2.0078740157480315
1022: 58ms 2.003921568627451
2046: 89ms 2.0019569471624266
4094: 191ms 2.0009775171065494
8190: 364ms 2.0004885197850513

Firefoxでしきい値をかなり上げる必要があったため、#126から始めました。

IEでは、次のような組み合わせが得られます。

256: 11ms 256
512: 26ms 2
1024: 77ms 2
1708: 113ms 1.66796875
2848: 154ms 1.6674473067915691
4748: 423ms 1.6671348314606742
7916: 944ms 1.6672283066554338

最初は2の累乗で、その後5分の3の累乗になります。

したがって、すべての一般的な実装では、配列に「通常の」方法を使用します(たとえば、 ropes で狂ったようになります)。

ベンチマークコードと フィドル を示します。

var arrayCount = 10000;

var dynamicArrays = [];

for(var j=0;j<arrayCount;j++)
    dynamicArrays[j] = [];

var lastLongI = 1;

for(var i=0;i<10000;i++)
{
    var before = Date.now();
    for(var j=0;j<arrayCount;j++)
        dynamicArrays[j][i] = i;
    var span = Date.now() - before;
    if (span > 10)
    {
      console.log(i + ": " + span + "ms" + " " + (i / lastLongI));
      lastLongI = i;
    }
}
0
John