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


105

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

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

また、配列がC ++配列のように扱われる場合があることも理解しています(つまり、高速なランダムインデックス付け、削除の遅延、サイズ変更)。また、オブジェクトのように扱われる場合もあります(高速なインデックス作成、高速な挿入/削除、より多くのメモリ)。そして、時々それらはリンクされたリストとして保存されるかもしれません(すなわち、遅いランダムなインデックス付け、最初/最後の速い削除/挿入)

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

より具体的には、それがパフォーマンスに与える影響:

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

詳細についての記事やリンクも同様にいただければ幸いです。:)

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

たとえば、次のような配列を作成するとします...

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が使用された場合...それほど悪くはありません。


2
訪問jsperf.com、およびテストケースを作成します。
Rob W

2
@RobWここでは、JITコンパイラーがどのように機能するか、データで何が行われているのかについての知識が必要な単純なテストで説明できる以上のものがあります。時間を見つけたら答えを追加しますが、うまくいけば、誰か他の人が骨の折れる時間に入る時間があるでしょう。また、このリンクはここに残しておきたいと思います
Incognito

私が話しているJITのことは、オブジェクトの「形状」、または定義された要素間に未定義の値を持つ配列のようなものです。また、最近型固有化機能で実験されました...配列固有のメソッドは、プロトタイプが操作されたかどうかも同様です。別のデータ型のAFAIKに切り替える「知る」ようなものはありません。
シークレットモードで

1
さまざまなオプティマイザと内部システムがどのように機能するかについて、Googleの担当者が話し合っています。そして、それらのために最適化する方法。(ゲーム用!)youtube.com/watch?v=XAqIpGU8ZZk
PicoCreator

回答:


279

これらの問題(およびその他)を正確に調査するために、テストスイートを作成しましアーカイブコピー)。

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

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

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

概要は以下の通りです

  • V8アレイは高速、非常に高速
  • 配列のプッシュ/ポップ/シフトは、同等のオブジェクトよりも約20倍以上高速です。
  • 意外と Array.shift()、配列のpopよりも約6倍高速ですが、オブジェクト属性の削除よりも約100倍高速です。
  • 面白いことに、ほぼ20(動的配列)から10(固定配列)回以上Array.push( data );高速ですArray[nextIndex] = data
  • Array.unshift(data) 予想どおり遅く、新しいプロパティの追加より約5倍遅くなります。
  • 値をnullにするarray[index] = null方が削除するよりも速いdelete array[index]、配列内の値(未定義)を約4x ++速くなります。
  • 驚くべきことに、オブジェクトの値をnullにobj[attr] = nullすると、属性を削除するよりも約2倍遅くなりますdelete obj[attr]
  • 当然のことながら、ミッドアレイ Array.splice(index,0,data)は非常に遅いです。
  • 驚くべきことに、Array.splice(index,1,data)最適化されており(長さの変更なし)、スプライスよりも100倍高速です。Array.splice(index,0,data)
  • 当然のことながら、divLinkedListは、dll.splice(index,1)削除を除いて、すべてのセクターの配列よりも劣っています(テストシステムを破壊した場合)。
  • 最大の驚きそれをすべての[jjrvが指摘したように]、V8アレイ書き込みが若干速くV8は= O読み込むよりもあります

注:これらのメトリックは、v8が「完全に最適化」しない大規模な配列/オブジェクトにのみ適用されます。配列/オブジェクトのサイズが任意のサイズ(24?)未満の場合、非常に孤立した最適化されたパフォーマンスケースが存在する可能性があります。詳細は、いくつかのGoogle IOビデオで広く見ることができます。

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

更新されたメモ(2012年12月): Googleの担当者は、Chrome自体の内部動作(linkedlist配列から固定配列に切り替わるときなど)とそれらを最適化する方法を説明するビデオをyoutubeに公開しています。詳しくは、GDC 2012:コンソールからChromeへをご覧ください。


2
これらの結果の一部は非常に奇妙に見えます。たとえば、Chromeアレイでは書き込みは読み取りより約10倍高速ですが、Firefoxでは反対です。ブラウザーJITがテスト全体を最適化していない場合がありますか?
jjrv

1
@jjrv good gosh = O you ...不十分に最適化された読み取り、または大幅に最適化された書き込み(即時バッファーに書き込みますか?)の場合が考えられます...調査する価値があります:lol
PicoCreator

2
ただアレイ上のビデオの議論で正確なポイントを追加したい:youtube.com/...は
badunk

1
JsPerfサイトはもう存在しません:(
JustGoscha

1
@JustGoschaわかりました、thx for the info:私はそれをgoogleキャッシュから再作成して修正しました。
PicoCreator 2014年

5

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

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

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

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

配列操作に対する最近の変更への特定のリンクは、ここで役立ちます。

少し余分なものとして、JS自体に実装されたV8のソースから直接配列ポップと配列プッシュを次に示します。

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;
}

1

成長する配列に関する実装の動作の問題に関する調査で既存の回答を補足したいと思います。それらが「通常の」方法でそれらを実装する場合、実装がコピーする時点で、まれな散在する低速プッシュを伴う多くの高速プッシュが表示されます。 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

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

したがって、最初の数値は挿入された要素(最初の行は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の累乗に移動します。

そのため、すべての一般的な実装では、配列に対して「通常の」方法を使用します(たとえば、ロープに夢中になるのではなく)。

これがベンチマークコードで、ここがフィドルです。

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

node.js 0.10(v8でビルド)で実行していると、CPU使用率がワークロードに対して過度に見えるようになりました。私は、配列内の文字列の存在をチェックしていた関数のパフォーマンスの問題を追跡しました。だから私はいくつかのテストを実行しました。

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

91kのエントリを配列に読み込む(検証とプッシュを使用)方が、obj [key] = valueを設定するよりも高速です。

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

  • 検索構成に87.56秒かかりました(配列)
  • 検索構成に0.21秒かかりました(オブジェクト)

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.