Qual è la performance di oggetti / array in JavaScript? (specificamente per Google V8)


105

Le prestazioni associate agli array e agli oggetti in JavaScript (in particolare Google V8) sarebbero molto interessanti da documentare. Non trovo alcun articolo completo su questo argomento da nessuna parte su Internet.

Capisco che alcuni oggetti utilizzano le classi come struttura dati sottostante. Se ci sono molte proprietà, a volte viene trattata come una tabella hash?

Capisco anche che gli array a volte vengono trattati come array C ++ (cioè indicizzazione casuale veloce, eliminazione e ridimensionamento lenti). E, altre volte, sono trattati più come oggetti (indicizzazione veloce, inserimento / rimozione veloce, più memoria). E, forse a volte vengono memorizzati come elenchi collegati (ad esempio indicizzazione casuale lenta, rimozione / inserimento veloce all'inizio / alla fine)

Qual è la performance precisa dei recuperi e delle manipolazioni di array / oggetti in JavaScript? (specificamente per Google V8)

Più specificamente, qual è l'impatto sulle prestazioni di:

  • Aggiunta di una proprietà a un oggetto
  • Rimozione di una proprietà da un oggetto
  • Indicizzazione di una proprietà in un oggetto
  • Aggiunta di un elemento a un array
  • Rimozione di un elemento da un array
  • Indicizzazione di un elemento in un array
  • Chiamata a Array.pop ()
  • Chiamare Array.push ()
  • Chiamata a Array.shift ()
  • Chiamata a Array.unshift ()
  • Chiamata a Array.slice ()

Anche qualsiasi articolo o collegamento per maggiori dettagli sarebbe apprezzato. :)

EDIT: Mi chiedo davvero come funzionano gli array e gli oggetti JavaScript sotto il cofano. Inoltre, in quale contesto il motore V8 "sa" di "passare" a un'altra struttura dati?

Ad esempio, supponiamo di creare un array con ...

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

Cosa sta succedendo davvero qui?

Oppure ... che ne dici di questo ... ???

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

Per gli array convenzionali, le prestazioni sarebbero terribili; considerando che, se è stata utilizzata una LinkedList ... non così male.


2
Visita jsperf.com e crea casi di test.
Rob W

2
@RobW C'è molto di più in gioco qui che semplici test possono spiegare che richiede la conoscenza di come funzionano i compilatori JIT e cosa viene fatto con i dati. Se trovo un po 'di tempo aggiungerò una risposta, ma spero che qualcun altro avrà il tempo di entrare nel nocciolo duro. Inoltre, vorrei lasciare questo link qui
Incognito

Le cose JIT di cui sto parlando sono cose come la "forma" di un oggetto, o array con valori indefiniti tra elementi definiti, così come i più recentemente sperimentati con caratteristiche di specializzazione del tipo ... i metodi specifici degli array possono dipendere dall'uso come così come se il prototipo sia stato manipolato o meno. Non esiste il "sapere di" passare a un altro tipo di dati AFAIK.
Incognito

1
Ci sono rappresentanti di Google che discutono su come funzionano i vari ottimizzatori e il sistema interno. E come ottimizzare per loro. (per i giochi!) youtube.com/watch?v=XAqIpGU8ZZk
PicoCreator

Risposte:


279

Ho creato una suite di test, proprio per esplorare queste problematiche (e non solo) ( copia archiviata ).

E in questo senso, puoi vedere i problemi di prestazioni in questo tester di oltre 50 casi di test (ci vorrà molto tempo).

Inoltre, come suggerisce il nome, esplora l'uso dell'utilizzo della natura dell'elenco collegato nativo della struttura DOM.

(Attualmente inattivo, ricostruito in corso) Maggiori dettagli sul mio blog in merito .

Il riepilogo è il seguente

  • V8 Array è veloce, MOLTO VELOCE
  • Il push / pop / shift dell'array è ~ circa 20 volte più veloce di qualsiasi equivalente oggetto.
  • Sorprendentemente Array.shift()è veloce ~ circa 6 volte più lento di un pop di un array, ma è ~ circa 100 volte più veloce della cancellazione di un attributo di un oggetto.
  • In modo divertente, Array.push( data );è più veloce di Array[nextIndex] = dataquasi 20 volte (array dinamico) fino a 10 volte (array fisso).
  • Array.unshift(data) è più lento come previsto ed è circa 5 volte più lento di una nuova proprietà aggiunta.
  • Annullare il valore array[index] = nullè più veloce che eliminarlo delete array[index](non definito) in un array di ~ circa 4x ++ più velocemente.
  • Sorprendentemente, annullare un valore in un oggetto è obj[attr] = nullcirca 2 volte più lento della semplice eliminazione dell'attributodelete obj[attr]
  • Non sorprende che il mid array Array.splice(index,0,data)sia lento, molto lento.
  • Sorprendentemente, Array.splice(index,1,data)è stato ottimizzato (nessuna variazione di lunghezza) ed è 100 volte più veloce della semplice giunzioneArray.splice(index,0,data)
  • non sorprende che divLinkedList sia inferiore a un array su tutti i settori, tranne la dll.splice(index,1)rimozione (dove ha rotto il sistema di test).
  • LA PIÙ GRANDE SORPRESA di tutto [come ha sottolineato jjrv], le scritture dell'array V8 sono leggermente più veloci delle letture V8 = O

Nota: queste metriche si applicano solo a array / oggetti di grandi dimensioni che la v8 non "ottimizza completamente". Possono esserci casi di prestazioni ottimizzate molto isolati per dimensioni di array / oggetti inferiori a una dimensione arbitraria (24?). Maggiori dettagli possono essere visti ampiamente in diversi video di Google IO.

Nota 2: questi meravigliosi risultati sulle prestazioni non sono condivisi tra i browser, in particolare *cough*IE. Anche il test è enorme, quindi devo ancora analizzare e valutare completamente i risultati: modificalo in =)

Nota aggiornata (dicembre 2012): i rappresentanti di Google hanno video su YouTube che descrivono il funzionamento interno di Chrome stesso (come quando passa da un array di elenchi collegati a un array fisso, ecc.) E su come ottimizzarli. Vedi GDC 2012: dalla console a Chrome per ulteriori informazioni.


2
Alcuni di questi risultati sembrano molto strani. Ad esempio, in Chrome le scritture di array sono circa 10 volte più veloci delle letture, mentre in Firefox è il contrario. Sei sicuro che il JIT del browser non stia ottimizzando l'intero test in alcuni casi?
jjrv

1
@jjrv good gosh = O hai ragione ... ho persino aggiornato ogni caso di scrittura in modo che sia univoco in modo incrementale, per prevenire JIT ... E onestamente, a meno che l'ottimizzazione JIT non sia così buona (cosa che trovo difficile da credere), potrebbe essere solo un caso di lettura scarsamente ottimizzata o scritture fortemente ottimizzate (scrivere nel buffer immediato?) ... che vale la pena indagare: lol
PicoCreator

2
volevo solo aggiungere il punto esatto nella discussione video sugli array: youtube.com/…
badunk

1
Il sito JsPerf non esiste più :(
JustGoscha

1
@ JustGoscha ok, grazie per le informazioni: l'ho riparato di nuovo ricreandolo dalla cache di Google.
PicoCreator

5

A un livello di base che rimane all'interno dei regni di JavaScript, le proprietà sugli oggetti sono entità molto più complesse. È possibile creare proprietà con setter / getter, con enumerabilità, scrivibilità e configurabilità diverse. Un elemento in un array non può essere personalizzato in questo modo: o esiste o no. A livello di motore sottostante ciò consente un'ottimizzazione molto maggiore in termini di organizzazione della memoria che rappresenta la struttura.

In termini di identificazione di un array da un oggetto (dizionario), i motori JS hanno sempre creato linee esplicite tra i due. Ecco perché c'è una moltitudine di articoli sui metodi per provare a creare un oggetto simile a un Array semi-falso che si comporta come uno ma consente altre funzionalità. Il motivo per cui esiste anche questa separazione è perché i motori JS stessi memorizzano i due in modo diverso.

Le proprietà possono essere memorizzate su un oggetto array, ma questo dimostra semplicemente come JavaScript insiste nel rendere ogni cosa un oggetto. I valori indicizzati in una matrice vengono archiviati in modo diverso da qualsiasi proprietà che si decide di impostare sull'oggetto matrice che rappresenta i dati della matrice sottostante.

Ogni volta che si utilizza un oggetto array legittimo e si utilizza uno dei metodi standard per manipolare quell'array, si raggiungono i dati dell'array sottostante. In V8 in particolare, questi sono essenzialmente gli stessi di un array C ++, quindi verranno applicate queste regole. Se per qualche motivo stai lavorando con un array che il motore non è in grado di determinare con sicurezza è un array, allora sei su un terreno molto più instabile. Con le versioni recenti di V8 c'è però più spazio per lavorare. Ad esempio, è possibile creare una classe che abbia Array.prototype come prototipo e ottenere comunque un accesso efficiente ai vari metodi di manipolazione degli array nativi. Ma questo è un cambiamento recente.

Link specifici alle recenti modifiche alla manipolazione degli array possono tornare utili qui:

Come extra, ecco Array Pop e Array Push direttamente dalla fonte di V8, entrambi implementati nello stesso 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;
}

1

Vorrei integrare le risposte esistenti con un'indagine sulla domanda su come si comportano le implementazioni riguardo agli array in crescita: se li implementano nel modo "normale", si vedrebbero molti push rapidi con push lenti rari e intervallati a quel punto l'implementazione copia la rappresentazione interna dell'array da un buffer a uno più grande.

Puoi vedere questo effetto molto bene, questo è da 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

Anche se ogni push è profilato, l'output contiene solo quelli che richiedono tempo al di sopra di una certa soglia. Per ogni test ho personalizzato la soglia per escludere tutte le spinte che sembrano rappresentare le spinte veloci.

Quindi il primo numero rappresenta quale elemento è stato inserito (la prima riga è per il 17 ° elemento), il secondo è quanto tempo ci è voluto (per molti array il benchmark è fatto in parallelo), e l'ultimo valore è la divisione del primo numero da quello di quello nella prima riga.

Tutte le linee con un tempo di esecuzione inferiore a 2 ms sono escluse per Chrome.

Puoi vedere che Chrome aumenta la dimensione dell'array in potenze di 1.5, più un po 'di offset per tenere conto di piccoli array.

Per Firefox, è una potenza di due:

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

Ho dovuto alzare un po 'la soglia in Firefox, ecco perché partiamo dal numero 126.

Con IE, otteniamo un mix:

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

All'inizio è una potenza di due e poi passa a una potenza di cinque terzi.

Quindi tutte le implementazioni comuni usano il modo "normale" per gli array (invece di impazzire con le corde , per esempio).

Ecco il codice benchmark ed ecco il violino in cui si trova.

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

Durante l'esecuzione con node.js 0.10 (costruito su v8) vedevo un utilizzo della CPU che sembrava eccessivo per il carico di lavoro. Ho tracciato un problema di prestazioni a una funzione che stava verificando l'esistenza di una stringa in un array. Quindi ho eseguito alcuni test.

  • caricato 90.822 host
  • il caricamento della configurazione ha richiesto 0,087 secondi (array)
  • il caricamento della configurazione ha richiesto 0,152 secondi (oggetto)

Il caricamento di 91k voci in un array (con validate & push) è più veloce rispetto all'impostazione di obj [key] = value.

Nel test successivo, ho cercato una volta ogni hostname nell'elenco (91k iterazioni, per calcolare la media del tempo di ricerca):

  • la ricerca della configurazione ha impiegato 87,56 secondi (array)
  • la ricerca della configurazione ha impiegato 0,21 secondi (oggetto)

L'applicazione qui è Haraka (un server SMTP) e carica host_list una volta all'avvio (e dopo le modifiche) e successivamente esegue questa ricerca milioni di volte durante il funzionamento. Il passaggio a un oggetto è stata un'enorme vittoria in termini di prestazioni.

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.