web-dev-qa-db-ja.com

なぜarray.Pushは時々array [n] = valueより速いのですか?

いくつかのコードをテストした結果として、array.Pushメソッドの使用速度と直接アドレス指定(array [n] = value)の速度を比較する小さな関数を作成しました。驚いたことに、特にFirefoxで、場合によってはChromeでPushメソッドが高速になることがよくありました。好奇心から:誰もそれについて説明がありますか?テストを見つけることができます@ このページ (「配列メソッド比較」をクリック)

68
KooiInc

あらゆる種類の要因が作用します。ほとんどのJS実装は、後で必要になった場合にスパースストレージに変換するフラット配列を使用します。

基本的に、スパースになるという決定は、設定されている要素と、フラットを維持するために無駄になるスペースの量に基づいたヒューリスティックです。

あなたの場合、最後の要素を最初に設定しています。つまり、JSエンジンはnの長さを必要とするが、単一の要素のみを含む配列を見ることになります。 nが十分に大きい場合、これはすぐに配列をスパース配列にします-ほとんどのエンジンでは、これはすべての後続の挿入が遅いスパース配列の場合を取ることを意味します。

インデックス0からインデックスn-1に配列を埋めるテストを追加する必要があります。はるかに高速であるはずです。

@Christophに対応し、先延ばしにしたいという要望から、ここでは配列が(一般に)JSで実装される方法について説明します-詳細はJSエンジンごとに異なりますが、一般的な原則は同じです。

すべてのJS Objects(文字列、数字、true、false、undefined、またはnullではありません)はベースオブジェクトタイプから継承します-正確な実装は異なります。 C++の継承、またはCでの手動(いずれかの方法でそれを行うことには利点があります)-ベースオブジェクトタイプは、デフォルトのプロパティアクセスメソッドを定義します。

interface Object {
    put(propertyName, value)
    get(propertyName)
private:
    map properties; // a map (tree, hash table, whatever) from propertyName to value
}

このオブジェクト型は、すべての標準プロパティアクセスロジック、プロトタイプチェーンなどを処理します。その後、配列の実装は

interface Array : Object {
    override put(propertyName, value)
    override get(propertyName)
private:
    map sparseStorage; // a map between integer indices and values
    value[] flatStorage; // basically a native array of values with a 1:1
                         // correspondance between JS index and storage index
    value length; // The `length` of the js array
}

JSで配列を作成すると、エンジンは上記のデータ構造に似たものを作成します。オブジェクトをArrayインスタンスに挿入すると、Arrayのputメソッドは、プロパティ名が0〜2 ^ 32の整数(または「121」、「2341」などの整数に変換できる)かどうかを確認します。 -1(または、おそらく2 ^ 31-1、私は正確に忘れます)。そうでない場合は、putメソッドが基本Object実装に転送され、標準[[Put]]ロジックが実行されます。それ以外の場合、値は配列自体のストレージに配置されます。データが十分にコンパクトな場合、エンジンはフラット配列ストレージを使用します。その場合、挿入(および取得)は単なる標準の配列インデックス操作です。ストレージをスパースし、マップを使用してpropertyNameから値の場所を取得します。

変換が発生した後、JSエンジンが現在スパースストレージからフラットストレージに変換されているかどうかは正直わかりません。

Anyhoo、それは何が起こるかについてのかなり高レベルの概要であり、より厄介な詳細の多くを省きますが、それは一般的な実装パターンです。追加のストレージとput/getのディスパッチ方法の詳細はエンジンごとに異なりますが、これは設計/実装を実際に説明できる最も明確なものです。

マイナーな追加ポイント。ES仕様ではpropertyNameを文字列として参照していますが、JSエンジンは整数ルックアップにも特化する傾向があるため、someObject[someInteger]は、整数のプロパティを持つオブジェクトを見ている場合、整数を文字列に変換しません。配列、文字列、およびDOMタイプ(NodeListsなど)。

83
olliej

これらは私がテストで得た結果です

safariの場合:

  • Array.Push(n)1,000,000値:0.124秒
  • Array [n .. 0] =値(降順)1,000,000値:3.697秒
  • Array [0 .. n] =値(昇順)1,000,000値:0.073秒

fireFoxの場合:

  • Array.Push(n)1,000,000値:0.075秒
  • Array [n .. 0] =値(降順)1,000,000値:1.193秒
  • Array [0 .. n] =値(昇順)1,000,000値:0.055秒

iE7の場合:

  • Array.Push(n)1,000,000値:2.828秒
  • Array [n .. 0] =値(降順)1,000,000値:1.141秒
  • Array [0 .. n] =値(昇順)1,000,000値:7.984秒

あなたのテストによると、PushメソッドはIE7でより良いようです(大きな違い)、そして他のブラウザでは、差は小さく、配列に要素を追加するための実際の最良の方法はPushメソッドのようです。

しかし、別の 簡単なテストスクリプト を作成して、どのメソッドが配列に値を追加するのが速いかをチェックしました。結果は本当に驚きました。 Array.Pushを使用する場合に比べて高速であるため、何を言うべきか、または何を考えるべきかが本当にわかりません。

ところで:私のIE7では、スクリプトが停止し、ブラウザは続行するかどうかを尋ねます(典型的なIEスクリプト?... ")ループを少し減らすために再定義します。

10
Marco Demaio

Push()は、より一般的な[[Put]]の特殊なケースであるため、さらに最適化できます。

配列オブジェクトで[[Put]]を呼び出すときは、配列インデックスを含むすべてのプロパティ名が文字列であるため、引数を最初に符号なし整数に変換する必要があります。次に、長さを増やす必要があるかどうかを判断するために、配列の長さプロパティと比較する必要があります。プッシュする場合、そのような変換や比較を行う必要はありません。配列のインデックスとして現在の長さを使用し、それを増やしてください。

もちろん、ランタイムに影響する他の事柄もあります。たとえば、Push()の呼び出しは、[]を介した[[Put]]の呼び出しよりも遅くする必要があります。


Olliejが指摘したように、実際のECMAScript実装は変換を最適化します。つまり、数値プロパティ名の場合、文字列からuintへの変換は行われず、単純な型チェックのみが行われます。その影響は当初想定していたよりも小さいものの、基本的な想定は依然として維持されるはずです。

6
Christoph

直接割り当てがプッシュよりもはるかに高速であることを確認する優れたテストベッドを次に示します。 http://jsperf.com/array-direct-assignment-vs-Push

編集:累積結果データの表示に問題があるようですが、うまくいけばすぐに修正されるでしょう。

4
Timo Kähkönen