Javascriptセットとアレイのパフォーマンス


90

セットはJavascriptに比較的新しいためかもしれませんが、StackOやその他の場所で、Javascriptの2つのパフォーマンスの違いについて説明している記事を見つけることができませんでした。では、パフォーマンスの観点から、2つの違いは何ですか?具体的には、削除、追加、反復に関してです。


1
それらを互換的に使用することはできません。したがって、それらを比較することはほとんど意味がありません。
zerkms 2016

あなたはとの比較について話しているSet[]{}
2016

2
追加と反復はあまり違いはありません。削除と(最も重要なのは)ルックアップは違いを生みます。
ベルギ2016


3
@zerkms —厳密には、配列も順序付けられていませんが、インデックスを使用すると、配列がそうであるかのように扱われます。;-)セット内の値のシーケンスは、挿入順序で保持されます。
RobG 2016

回答:


102

わかりました。配列とセットの両方から要素を追加、反復、削除することをテストしました。10000個の要素を使用して「小さい」テストを実行し、100000個の要素を使用して「大きい」テストを実行しました。結果は次のとおりです。

コレクションへの要素の追加

追加する要素の数に関係なく、.pusharrayメソッドは.addsetメソッドよりも約4倍高速であるように見えます。

コレクション内の要素の反復と変更

テストのこの部分では、forループを使用して配列を反復処理し、ループを使用for ofしてセットを反復処理しました。繰り返しになりますが、配列の反復処理は高速でした。今回は、「小さい」テストでは2倍、「大きい」テストではほぼ4倍の時間がかかったため、指数関数的に見えるようになります。

コレクションから要素を削除する

今、これはそれが面白くなるところです。forループと.splice配列からいくつかの要素を削除するためにループを組み合わせて使用し、セットからいくつかの要素を削除するために使用for of.deleteました。「小さい」テストの場合、セットからアイテムを削除する方が約3倍高速でした(2.6ミリ秒対7.1ミリ秒)が、配列からアイテムを削除するのに1955.1ミリ秒かかった「大きい」テストでは、状況が大幅に変わりました。セットからそれらを削除するのに83.6ミリ秒かかり、23倍速くなりました。

結論

10k要素では、両方のテストが同等の時間(配列:16.6 ms、セット:20.7 ms)で実行されましたが、100k要素を処理する場合、セットが明確な勝者でした(配列:1974.8 ms、セット:83.6 ms)が、削除したためです。操作。それ以外の場合、アレイは高速でした。それがなぜなのか正確には言えませんでした。

配列が作成されて入力され、いくつかの要素が削除されるセットに変換された後、セットが配列に再変換される、いくつかのハイブリッドシナリオを試してみました。これを行うと、配列内の要素を削除するよりもはるかに優れたパフォーマンスが得られますが、セットとの間で転送するために必要な追加の処理時間は、セットではなく配列にデータを入力することの利点を上回ります。結局、セットだけを扱う方が速いです。それでも、重複のないビッグデータのデータコレクションとして配列を使用することを選択した場合、1つの要素から多くの要素を削除する必要がある場合は、パフォーマンスの面で有利になる可能性があるというのは興味深い考えです。操作。配列をセットに変換するには、削除操作を実行して、セットを配列に戻します。

配列コード:

var timer = function(name) {
  var start = new Date();
  return {
    stop: function() {
      var end = new Date();
      var time = end.getTime() - start.getTime();
      console.log('Timer:', name, 'finished in', time, 'ms');
    }
  }
};

var getRandom = function(min, max) {
  return Math.random() * (max - min) + min;
};

var lastNames = ['SMITH', 'JOHNSON', 'WILLIAMS', 'JONES', 'BROWN', 'DAVIS', 'MILLER', 'WILSON', 'MOORE', 'TAYLOR', 'ANDERSON', 'THOMAS'];

var genLastName = function() {
  var index = Math.round(getRandom(0, lastNames.length - 1));
  return lastNames[index];
};

var sex = ["Male", "Female"];

var genSex = function() {
  var index = Math.round(getRandom(0, sex.length - 1));
  return sex[index];
};

var Person = function() {
  this.name = genLastName();
  this.age = Math.round(getRandom(0, 100))
  this.sex = "Male"
};

var genPersons = function() {
  for (var i = 0; i < 100000; i++)
    personArray.push(new Person());
};

var changeSex = function() {
  for (var i = 0; i < personArray.length; i++) {
    personArray[i].sex = genSex();
  }
};

var deleteMale = function() {
  for (var i = 0; i < personArray.length; i++) {
    if (personArray[i].sex === "Male") {
      personArray.splice(i, 1)
      i--
    }
  }
};

var t = timer("Array");

var personArray = [];

genPersons();

changeSex();

deleteMale();

t.stop();

console.log("Done! There are " + personArray.length + " persons.")

セットコード:

var timer = function(name) {
    var start = new Date();
    return {
        stop: function() {
            var end  = new Date();
            var time = end.getTime() - start.getTime();
            console.log('Timer:', name, 'finished in', time, 'ms');
        }
    }
};

var getRandom = function (min, max) {
  return Math.random() * (max - min) + min;
};

var lastNames = ['SMITH','JOHNSON','WILLIAMS','JONES','BROWN','DAVIS','MILLER','WILSON','MOORE','TAYLOR','ANDERSON','THOMAS'];

var genLastName = function() {
    var index = Math.round(getRandom(0, lastNames.length - 1));
    return lastNames[index];
};

var sex = ["Male", "Female"];

var genSex = function() {
    var index = Math.round(getRandom(0, sex.length - 1));
    return sex[index];
};

var Person = function() {
	this.name = genLastName();
	this.age = Math.round(getRandom(0,100))
	this.sex = "Male"
};

var genPersons = function() {
for (var i = 0; i < 100000; i++)
	personSet.add(new Person());
};

var changeSex = function() {
	for (var key of personSet) {
		key.sex = genSex();
	}
};

var deleteMale = function() {
	for (var key of personSet) {
		if (key.sex === "Male") {
			personSet.delete(key)
		}
	}
};

var t = timer("Set");

var personSet = new Set();

genPersons();

changeSex();

deleteMale();

t.stop();

console.log("Done! There are " + personSet.size + " persons.")


1
セットの値はデフォルトで一意であることに注意してください。したがって、[1,1,1,1,1,1]配列の長さが6の場合、セットのサイズは1になります。このセットの特性により、コードは実際には、実行ごとにサイズが100,000アイテムよりも大きく異なるサイズのセットを生成する可能性があります。スクリプト全体が実行されるまでセットのサイズが表示されないため、おそらく気付かないでしょう。
KyleFarris 2016年

6
@KyleFarris私が間違っていない限り、これはあなたの例のようにセットに重複があった場合に当てはまり[1, 1, 1, 1, 1]ますが、セット内の各アイテムは実際にはリストからランダムに生成された姓名を含むさまざまなプロパティを持つオブジェクトであるためです何百もの可能な名前、ランダムに生成された年齢、ランダムに生成された性別、およびその他のランダムに生成された属性...セットに2つの同一のオブジェクトがある可能性はほとんどありません。
snowfrogdev 2016年

3
実際、この場合は正しいです。セットは実際にはセット内のオブジェクトと区別されていないように見えるからです。したがって、実際に{foo: 'bar'}は、セットに同じ正確なオブジェクト10,000xを含めることもでき、サイズは10,000になります。アレイについても同じことが言えます。スカラー値(文字列、数値、ブール値など)でのみ一意のようです。
KyleFarris 2017年

13
セットには、オブジェクトの まったく同じコンテンツを{foo: 'bar'}何度も含めることができますが、まったく同じオブジェクト(参照)を含めることはできません。IMO微妙な違いを指摘する価値
SimpleVar

16
セットを使用する最も重要な理由である0(1)ルックアップの測定を忘れました。hasIndexOf
マグナス

67

観察

  • セット操作は、実行ストリーム内のスナップショットとして理解できます。
  • 私たちは決定的な代用品の前ではありません。
  • Setクラスの要素には、アクセス可能なインデックスがありません。
  • セットクラス配列クラスの補足であり、基本的な加算、削除、チェック、および反復操作を適用するコレクションを格納する必要があるシナリオで役立ちます。

パフォーマンスのテストをいくつか共有します。コンソールを開いて、以下のコードをコピーして貼り付けてみてください。

配列の作成(125000)

var n = 125000;
var arr = Array.apply( null, Array( n ) ).map( ( x, i ) => i );
console.info( arr.length ); // 125000

1.インデックスの検索

SetのhasメソッドをArrayindexOfと比較しました。

Array / indexOf(0.281ms)| セット/持っている(0.053ms)

// Helpers
var checkArr = ( arr, item ) => arr.indexOf( item ) !== -1;
var checkSet = ( set, item ) => set.has( item );

// Vars
var set, result;

console.time( 'timeTest' );
result = checkArr( arr, 123123 );
console.timeEnd( 'timeTest' );

set = new Set( arr );

console.time( 'timeTest' );
checkSet( set, 123123 );
console.timeEnd( 'timeTest' );

2.新しい要素を追加する

SetオブジェクトとArrayオブジェクトのaddメソッドとpushメソッドをそれぞれ比較します。

アレイ/プッシュ(1.612ms)| 設定/追加(0.006ms)

console.time( 'timeTest' );
arr.push( n + 1 );
console.timeEnd( 'timeTest' );

set = new Set( arr );

console.time( 'timeTest' );
set.add( n + 1 );
console.timeEnd( 'timeTest' );

console.info( arr.length ); // 125001
console.info( set.size ); // 125001

3.要素の削除

要素を削除するときは、ArrayとSetが同じ条件下で開始されないことに注意する必要があります。配列にはネイティブメソッドがないため、外部関数が必要です。

Array / deleteFromArr(0.356ms)| セット/削除(0.019ms)

var deleteFromArr = ( arr, item ) => {
    var i = arr.indexOf( item );
    i !== -1 && arr.splice( i, 1 );
};

console.time( 'timeTest' );
deleteFromArr( arr, 123123 );
console.timeEnd( 'timeTest' );

set = new Set( arr );

console.time( 'timeTest' );
set.delete( 123123 );
console.timeEnd( 'timeTest' );

ここで記事全文を読む


4
Array.indexOfは、それらが同等であるためにはArray.includesである必要があります。Firefoxで非常に異なる数値を取得しています。
kagronick

2
私は... Object.includes対Set.has比較に興味がある
レオポルドKristjanssonを

2
@LeopoldKristjansson比較テストは作成しませんでしたが、24kアイテムの配列を使用して本番サイトでタイミングを調整し、Array.includesからSet.hasに切り替えることでパフォーマンスが大幅に向上しました。
sedot

4

私の観察では、大きな配列の2つの落とし穴を念頭に置いて、セットの方が常に優れています。

a)配列からのセットの作成はfor、事前にキャッシュされた長さのループで実行する必要があります。

遅い(例:18ms) new Set(largeArray)

高速(例:6ms) const SET = new Set(); const L = largeArray.length; for(var i = 0; i<L; i++) { SET.add(largeArray[i]) }

b)for ofループよりも高速であるため、同じ方法で反復を行うことができます。

https://jsfiddle.net/0j2gkae7/5/を参照してください

現実の比較のために difference()intersection()union()およびuniq()40.000の要素を持つ(+そのiterateeの仲間など)


3

ベンチマークされた反復のスクリーンショットあなたの質問の反復部分について、私は最近このテストを実行し、Setが10,000アイテムの配列をはるかに上回っていることを発見しました(操作は同じ時間枠で約10倍発生する可能性があります)。そして、ブラウザに応じて、同様のテストでObject.hasOwnPropertyに勝つか負けます。

SetとObjectはどちらも、O(1)に償却されているように見える「has」メソッドを実行しますが、ブラウザーの実装によっては、1回の操作にかかる時間が長くなることも速くなることもあります。ほとんどのブラウザは、Set.has()よりも速くObjectにキーを実装しているようです。キーの追加チェックを含むObject.hasOwnPropertyでさえ、少なくともChrome v86ではSet.has()よりも約5%高速です。

https://jsperf.com/set-has-vs-object-hasownproperty-vs-array-includes/1

更新:2020年11月11日:https://jsbench.me/irkhdxnoqa/2

さまざまなブラウザ/環境で独自のテストを実行する場合。


同様に、配列にアイテムを追加するためのベンチマークと、セットおよび削除するためのベンチマークを追加します。


4
あなたの場合のように、これらのリンクが壊れている可能性があるため、回答にリンクを使用しないでください(公式ライブラリにリンクされている場合を除く)。あなたは、リンク404である
ギルEpshtain

リンクを使用しましたが、利用可能な場合は出力もコピーしました。彼らがリンク戦略をこんなに早く変えたのは残念です。
ザーゴールド

:スクリーンショットと新しいJSのパフォーマンスのウェブサイトで、今のポストを更新しましたjsbench.me
Zargold

0

プロパティルックアップのみ、書き込みはほとんどまたはゼロ

プロパティルックアップがあなたの主な関心事であるならば、ここにいくつかの数字があります。

JSBenchテストhttps://jsbench.me/3pkjlwzhbr/1

アレイ
  • for ループ
  • for ループ(反転)
  • array.includes(target)
セットする
  • set.has(target)
オブジェクト
  • obj.hasOwnProperty(target)
  • target in obj <-1.29%遅い
  • obj[target] <-最速
地図
  • map.has(target) <-2.94%遅い
2021年1月の 結果Chrome87

ここに画像の説明を入力してください

他のブラウザからの結果は大歓迎です。この回答を更新してください。このスプレッドシート
使用して、素敵なスクリーンショットを作成できます。

JSBenchテストはZargoldの回答から分岐しました。


-5
console.time("set")
var s = new Set()
for(var i = 0; i < 10000; i++)
  s.add(Math.random())
s.forEach(function(e){
  s.delete(e)
})
console.timeEnd("set")
console.time("array")
var s = new Array()
for(var i = 0; i < 10000; i++)
  s.push(Math.random())
s.forEach(function(e,i){
  s.splice(i)
})
console.timeEnd("array")

10Kアイテムに対するこれらの3つの操作により、次のことがわかりました。

set: 7.787ms
array: 2.388ms

@Bergiも最初はそう思っていましたが、そうです。
zerkms 2016

1
@zerkms:「work」を定義します:-)はい、配列はの後に空になりますforEachが、おそらく期待した方法ではありません。同等の動作が必要な場合は、それも必要s.forEach(function(e) { s.clear(); })です。
ベルギ2016

1
まあ、それは意図されたものではなく、何かをします:それはインデックスiと終わりの間のすべての要素を削除します。これはdelete、セットで行うこととは比較されません。
trincot 2016

@Bergiああ、それはたった2回の反復ですべてを削除します。私の悪い。
zerkms 2016

4
1回の繰り返しで。splice(0)配列を空にします。
trincot 2016
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.