Set Javascript vs prestazioni array


90

Forse perché i set sono relativamente nuovi per Javascript ma non sono stato in grado di trovare un articolo, su StackO o altrove, che parli della differenza di prestazioni tra i due in Javascript. Allora, qual è la differenza, in termini di prestazioni, tra i due? In particolare, quando si tratta di rimuovere, aggiungere e ripetere.


1
Non puoi usarli in modo intercambiabile. Quindi non ha molto senso confrontarli.
zerkms

stai parlando di confronto tra Sete []o {}?
scadenza il

2
L'aggiunta e l'iterazione non fanno molta differenza, la rimozione e, cosa più importante, la ricerca fanno la differenza.
Bergi


3
@ zerkms — rigorosamente, neanche gli Array sono ordinati, ma il loro uso di un indice consente loro di essere trattati come se lo fossero. ;-) La sequenza di valori in un Set viene mantenuta in ordine di inserimento.
RobG

Risposte:


102

Ok, ho testato l'aggiunta, l'iterazione e la rimozione di elementi sia da un array che da un set. Ho eseguito un test "piccolo", utilizzando 10.000 elementi e un test "grande", utilizzando 100.000 elementi. Ecco i risultati.

Aggiunta di elementi a una raccolta

Sembrerebbe che il file .push metodo array sia circa 4 volte più veloce di.add metodo set, indipendentemente dal numero di elementi aggiunti.

Iterazione e modifica di elementi in una raccolta

Per questa parte del test ho usato un forciclo per iterare sull'array e un filefor of ciclo per iterare sul set. Di nuovo, l'iterazione sull'array è stata più veloce. Questa volta sembrerebbe che sia esponenziale in quanto ci è voluto il doppio del tempo durante i test "piccoli" e quasi quattro volte durante i test "grandi".

Rimozione di elementi da una raccolta

Ora è qui che diventa interessante. Ho usato una combinazione di un forciclo e .spliceper rimuovere alcuni elementi dall'array e ho usato for ofe.delete per rimuovere alcuni elementi dal set. Per i test "piccoli", era circa tre volte più veloce rimuovere gli elementi dal set (2,6 ms contro 7,1 ms) ma le cose sono cambiate drasticamente per il test "grande" dove ci sono voluti 1955,1 ms per rimuovere gli elementi dall'array mentre solo ci sono voluti 83,6 ms per rimuoverli dal set, 23 volte più velocemente.

Conclusioni

A 10k elementi, entrambi i test hanno eseguito tempi comparabili (array: 16,6 ms, set: 20,7 ms) ma quando si trattava di 100k elementi, il set è stato il chiaro vincitore (array: 1974,8 ms, set: 83,6 ms) ma solo a causa della rimozione operazione. Altrimenti l'array era più veloce. Non saprei dire esattamente perché sia ​​così.

Ho giocato con alcuni scenari ibridi in cui un array è stato creato e popolato e poi convertito in un set in cui alcuni elementi sarebbero stati rimossi, il set sarebbe poi riconvertito in un array. Sebbene in questo modo si ottengono prestazioni molto migliori rispetto alla rimozione di elementi nell'array, il tempo di elaborazione aggiuntivo necessario per il trasferimento da e verso un set supera i vantaggi di popolare un array invece di un set. Alla fine, è più veloce gestire solo un set. Tuttavia, è un'idea interessante, che se si sceglie di utilizzare un array come raccolta di dati per alcuni big data che non hanno duplicati, potrebbe essere vantaggioso in termini di prestazioni, se è mai necessario rimuovere molti elementi in uno operazione, per convertire l'array in un set, eseguire l'operazione di rimozione e riconvertire l'insieme in un array.

Codice array:

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.")

Imposta codice:

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
Tieni presente che i valori di un insieme sono univoci per impostazione predefinita. Quindi, dove come [1,1,1,1,1,1]per un array avrebbe lunghezza 6, un set avrebbe dimensione 1. Sembra che il tuo codice potrebbe effettivamente generare insiemi di dimensioni estremamente diverse rispetto a 100.000 elementi di dimensione su ciascuna esecuzione a causa di questa caratteristica dei set. Probabilmente non l'hai mai notato perché non stai mostrando la dimensione del set fino a quando l'intero script non è stato eseguito.
KyleFarris

6
@ KyleFarris A meno che non mi sbagli, questo sarebbe vero se ci fossero duplicati nel set, come nel tuo esempio [1, 1, 1, 1, 1], ma poiché ogni elemento nel set è in realtà un oggetto con varie proprietà tra cui un nome e un cognome generati casualmente da un elenco di centinaia di nomi possibili, un'età generata casualmente, un sesso generato casualmente e altri attributi generati casualmente ... le probabilità di avere due oggetti identici nei set sono scarse o nulle.
snowfrogdev

3
In realtà, hai ragione in questo caso perché sembra che i set non si differenzino effettivamente dagli oggetti nel set. Quindi, in effetti potresti persino avere lo stesso oggetto esatto {foo: 'bar'}10.000x nel set e avrebbe una dimensione di 10.000. Lo stesso vale per gli array. Sembra che sia unico solo con valori scalari (stringhe, numeri, booleani, ecc.).
KyleFarris

13
Potresti avere lo stesso esatto contenuto di un oggetto {foo: 'bar'} molte volte nel Set, ma non lo stesso identico oggetto (riferimento). Vale la pena sottolineare la sottile differenza IMO
SimpleVar

16
Hai dimenticato la misura il motivo più importante per utilizzare un Set, la ricerca 0 (1). hasvs IndexOf.
Magnus

67

OSSERVAZIONI :

  • Le operazioni di set possono essere interpretate come istantanee all'interno del flusso di esecuzione.
  • Non siamo prima di un sostituto definitivo.
  • Gli elementi di una classe Set non hanno indici accessibili.
  • Set class è un complemento di classe Array , utile in quegli scenari in cui è necessario memorizzare una raccolta su cui applicare operazioni di base di addizione, eliminazione, controllo e iterazione.

Condivido alcuni test di performance. Prova ad aprire la tua console e copia e incolla il codice qui sotto.

Creazione di un array (125000)

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

1. Individuazione di un indice

Abbiamo confrontato il metodo has di Set con Array indexOf:

Array / indexOf (0.281ms) | Imposta / ha (0,053 ms)

// 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. Aggiunta di un nuovo elemento

Confrontiamo rispettivamente i metodi add e push degli oggetti Set e Array:

Array / push (1.612 ms) | Imposta / aggiungi (0,006 ms)

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. Eliminazione di un elemento

Quando si eliminano elementi, dobbiamo tenere presente che Array e Set non si avviano in condizioni uguali. Array non ha un metodo nativo, quindi è necessaria una funzione esterna.

Array / deleteFromArr (0,356 ms) | Imposta / rimuovi (0,019 ms)

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' );

Leggi l'articolo completo qui


4
Array.indexOf dovrebbe essere Array.includes affinché siano equivalenti. Ricevo numeri molto diversi su Firefox.
kagronick

2
Sarei interessato al confronto tra Object.includes e Set.has ...
Leopold Kristjansson

2
@LeopoldKristjansson Non ho scritto un test di confronto, ma abbiamo fatto i tempi in un sito di produzione con array con 24k elementi e il passaggio da Array.includes a Set. È stato un enorme aumento delle prestazioni!
sedot

4

La mia osservazione è che un set è sempre migliore con due insidie ​​per array di grandi dimensioni in mente:

a) La creazione di set da array deve essere eseguita in un forciclo con una lunghezza precablata.

lento (ad es. 18 ms) new Set(largeArray)

veloce (ad es. 6 ms) const SET = new Set(); const L = largeArray.length; for(var i = 0; i<L; i++) { SET.add(largeArray[i]) }

b) L'iterazione potrebbe essere eseguita allo stesso modo perché è anche più veloce di a for of ciclo ...

Vedi https://jsfiddle.net/0j2gkae7/5/

per un confronto reale vita difference(), intersection(), union()e uniq()(+ loro compagni iteratee etc.) con 40.000 elementi


3

Screenshot dell'iterazione con benchmarkPer la parte di iterazione della tua domanda, di recente ho eseguito questo test e ho scoperto che Set ha superato di gran lunga un array di 10.000 elementi (circa 10 volte le operazioni potrebbero avvenire nello stesso lasso di tempo). E a seconda del browser o ha battuto o perso contro Object.hasOwnProperty in un test simile.

Sia Set che Object hanno il loro metodo "has" che esegue ciò che sembra essere ammortizzato in O (1), ma a seconda dell'implementazione del browser una singola operazione potrebbe richiedere più tempo o più velocemente. Sembra che la maggior parte dei browser implementa la chiave in Object più velocemente di Set.has (). Anche Object.hasOwnProperty che include un controllo aggiuntivo sulla chiave è circa il 5% più veloce di Set.has () almeno per me su Chrome v86.

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

Aggiornamento: 11/11/2020: https://jsbench.me/irkhdxnoqa/2

Nel caso in cui desideri eseguire i tuoi test con diversi browser / ambienti.


Allo stesso modo aggiungerò un benchmark per l'aggiunta di elementi a un array rispetto a un set e la rimozione.


4
Si prega di non utilizzare collegamenti nelle risposte (a meno che non siano collegati a una libreria ufficiale) poiché questi collegamenti potrebbero essere interrotti, come è successo nel tuo caso. Il tuo link è 404.
Gil Epshtain

Ho usato un collegamento ma ho anche copiato l'output quando era disponibile. È un peccato che abbiano cambiato la loro strategia di collegamento così rapidamente.
Zargold

Aggiornato il post ora con uno screenshot e un nuovo sito Web sulle prestazioni di JS: jsbench.me
Zargold

0

Solo la ricerca della proprietà, poche o zero scritture

Se la ricerca di proprietà è la tua preoccupazione principale, ecco alcuni numeri.

JSBench verifica https://jsbench.me/3pkjlwzhbr/1

Vettore
  • for ciclo continuo
  • for loop (invertito)
  • array.includes(target)
Impostato
  • set.has(target)
Oggetto
  • obj.hasOwnProperty(target)
  • target in obj <- 1,29% più lento
  • obj[target] <- più veloce
Carta geografica
  • map.has(target) <- 2,94% più lento
Risultati di gennaio 2021, Chrome 87

inserisci qui la descrizione dell'immagine

I risultati di altri browser sono i benvenuti, aggiorna questa risposta.
Puoi usare questo foglio di calcolo per fare un bel screenshot.

Il test di JSBench è stato biforcato dalla risposta di 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")

Queste tre operazioni su 10.000 elementi mi hanno dato:

set: 7.787ms
array: 2.388ms

@Bergi è quello che pensavo anch'io inizialmente, ma è così.
zerkms

1
@zerkms: Definisci "work" :-) Sì, l'array sarà vuoto dopo forEach, ma probabilmente non nel modo che ti aspettavi. Se uno vuole un comportamento comparabile, dovrebbe esserlo s.forEach(function(e) { s.clear(); })anche lui.
Bergi

1
Bene, fa qualcosa, ma non ciò che è inteso: cancella tutti gli elementi tra l'indice i e la fine. Questo non è paragonabile a quello che deletefa sul set.
trincot

@Bergi oh giusto, rimuove tutto in sole 2 iterazioni. Colpa mia.
zerkms

4
In 1 iterazione. splice(0)svuota un array.
trincot
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.