Perché <= più lento di <usando questo snippet di codice in V8?


166

Sto leggendo le diapositive Breaking the Javascript Speed ​​Limit con V8 , e c'è un esempio come il codice qui sotto. Non riesco a capire perché <=sia più lento di< in questo caso, qualcuno può spiegarlo? Tutti i commenti sono apprezzati

Lento:

this.isPrimeDivisible = function(candidate) {
    for (var i = 1; i <= this.prime_count; ++i) {
        if (candidate % this.primes[i] == 0) return true;
    }
    return false;
} 

(Suggerimento: primes è un array di lunghezza prime_count)

Più veloce:

this.isPrimeDivisible = function(candidate) {
    for (var i = 1; i < this.prime_count; ++i) {
        if (candidate % this.primes[i] == 0) return true;
    }
    return false;
} 

[Ulteriori informazioni] il miglioramento della velocità è significativo, nel mio test dell'ambiente locale, i risultati sono i seguenti:

V8 version 7.3.0 (candidate) 

Lento:

 time d8 prime.js
 287107
 12.71 user 
 0.05 system 
 0:12.84 elapsed 

Più veloce:

time d8 prime.js
287107
1.82 user 
0.01 system 
0:01.84 elapsed

10
@DacreDenny La difficoltà computazionale di <=ed <è identica, sia in teoria che nell'attuazione effettiva in tutti i moderni processori (e interpreti).
TypeIA,

1
Ho letto il documento, c'è un maincodice che chiama quella funzione in un ciclo che scorre 25000volte, quindi nel complesso stai facendo molte meno iterazioni facendo quel cambiamento. Inoltre, se un array ha una lunghezza di 5, il tentativo di ottenere array[5]andrà oltre il suo limite fornendo un undefinedvalore poiché gli array iniziano a indicizzarsi 0.
Shidersz,

1
Sarebbe utile se questa domanda spiegasse quanto si ottiene un miglioramento della velocità (ad esempio, 5 volte più veloce) in modo che le persone non vengano respinte dall'iterazione aggiuntiva. Ho provato a trovare la velocità nelle diapositive, ma ce n'erano molte e ho avuto difficoltà a trovarlo, altrimenti lo avrei modificato da solo.
Captain Man,

@CaptainMan Hai ragione, l'esatto miglioramento della velocità è difficile da capire dalle diapositive perché coprono diversi problemi contemporaneamente. Ma nella mia conversazione con l'oratore dopo questo discorso, ha confermato che non è solo una piccola frazione del percento, come ci si potrebbe aspettare da un'iterazione aggiuntiva in questo caso di prova, ma una grande differenza: molte volte più veloce, forse un ordine di grandezza o più. E la ragione di ciò è che V8 ricade (o ricadde in quei giorni) nel formato di array de-ottimizzato quando si tenta di leggere al di fuori dei limiti di array.
Michael Geary,

3
Può essere utile confrontare una versione che utilizza <=ma che altrimenti agisce in modo identico alla <versione eseguendoi <= this.prime_count - 1 . Questo risolve sia il problema "extra iteration" che quello "one after the end of the array".
TheHansinator,

Risposte:


132

Lavoro su V8 ​​su Google e volevo fornire alcune informazioni aggiuntive sulle risposte e sui commenti esistenti.

Per riferimento, ecco l'esempio di codice completo dalle diapositive :

var iterations = 25000;

function Primes() {
  this.prime_count = 0;
  this.primes = new Array(iterations);
  this.getPrimeCount = function() { return this.prime_count; }
  this.getPrime = function(i) { return this.primes[i]; }
  this.addPrime = function(i) {
    this.primes[this.prime_count++] = i;
  }
  this.isPrimeDivisible = function(candidate) {
    for (var i = 1; i <= this.prime_count; ++i) {
      if ((candidate % this.primes[i]) == 0) return true;
    }
    return false;
  }
};

function main() {
  var p = new Primes();
  var c = 1;
  while (p.getPrimeCount() < iterations) {
    if (!p.isPrimeDivisible(c)) {
      p.addPrime(c);
    }
    c++;
  }
  console.log(p.getPrime(p.getPrimeCount() - 1));
}

main();

Innanzitutto, la differenza di prestazioni non ha nulla a che fare con gli operatori <e <=direttamente. Quindi per favore non saltare attraverso i cerchi solo per evitare <=nel tuo codice perché leggi su Stack Overflow che è lento --- non lo è!


In secondo luogo, la gente ha sottolineato che l'array è "bucato". Questo non era chiaro dallo snippet di codice nel post di OP, ma è chiaro quando si guarda il codice che inizializza this.primes:

this.primes = new Array(iterations);

Ciò si traduce in un array con un HOLEYtipo di elementi in V8, anche se l'array finisce completamente riempito / impaccato / contiguo. In generale, le operazioni su array bucati sono più lente di quelle su array compressi, ma in questo caso la differenza è trascurabile: equivale a 1 controllo Smi aggiuntivo ( piccolo intero ) ogni volta che colpiamo this.primes[i]il loop all'interno isPrimeDivisible. Nessun grosso problema!

TL; DR L'array HOLEYnon è il problema qui.


Altri hanno sottolineato che il codice legge fuori dai limiti. È generalmente raccomandato evitare la lettura oltre la lunghezza degli array e in questo caso avrebbe effettivamente evitato il massiccio calo delle prestazioni. Ma perché però? V8 è in grado di gestire alcuni di questi scenari fuori limite con solo un impatto minore sulle prestazioni.Cosa c'è di così speciale in questo caso particolare, allora?

La lettura fuori limite risulta this.primes[i]essereundefined su questa linea:

if ((candidate % this.primes[i]) == 0) return true;

E questo ci porta al vero problema : il% operatore viene ora utilizzato con operandi non interi!

  • integer % someOtherIntegerpuò essere calcolato in modo molto efficiente; I motori JavaScript possono produrre codice macchina altamente ottimizzato per questo caso.

  • integer % undefinedd'altra parte equivale a un modo meno efficiente Float64Mod, poiché undefinedè rappresentato come un doppio.

Lo snippet di codice può effettivamente essere migliorato modificando <=in< su questa riga:

for (var i = 1; i <= this.prime_count; ++i) {

... non perché <=sia in qualche modo un operatore superiore rispetto a <, ma solo perché questo evita i limiti di lettura fuori campo in questo caso particolare.


1
I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
Samuel Liew

1
Per essere completo al 100%, l'IC di caricamento con chiave per this.primes [i] in isPrimeDivisible diventa inaspettatamente megamorfico in V8. Sembra un bug: bugs.chromium.org/p/v8/issues/detail?id=8561
Mathias Bynens

226

Altre risposte e commenti menzionano che la differenza tra i due loop è che il primo esegue un'iterazione in più rispetto al secondo. Questo è vero, ma in un array che cresce fino a 25.000 elementi, un'iterazione più o meno farebbe solo una minuscola differenza. Come ipotesi da ballpark, se assumiamo che la lunghezza media man mano che cresce è di 12.500, la differenza che potremmo aspettarci dovrebbe essere di circa 1 / 12.500, o solo dello 0,008%.

La differenza di prestazioni qui è molto più grande di quanto sarebbe spiegato da quell'iterazione aggiuntiva e il problema è spiegato verso la fine della presentazione.

this.primes è un array contiguo (ogni elemento contiene un valore) e gli elementi sono tutti numeri.

Un motore JavaScript può ottimizzare tale array in modo che sia un semplice array di numeri effettivi, anziché un array di oggetti che contengono numeri ma potrebbero contenere altri valori o nessun valore. L'accesso al primo formato è molto più veloce: richiede meno codice e l'array è molto più piccolo, quindi si adatta meglio alla cache. Ma ci sono alcune condizioni che potrebbero impedire l'utilizzo di questo formato ottimizzato.

Una condizione sarebbe se mancano alcuni elementi dell'array. Per esempio:

let array = [];
a[0] = 10;
a[2] = 20;

Ora, qual è il valore di a[1]? Essa non ha alcun valore . (Non è nemmeno corretto affermare che ha il valore undefined: un elemento array contenenteundefined valore è diverso da un elemento array che manca del tutto.)

Non esiste un modo per rappresentarlo solo con i numeri, quindi il motore JavaScript è costretto a utilizzare il formato meno ottimizzato. Sea[1] contenesse un valore numerico come gli altri due elementi, l'array potrebbe essere potenzialmente ottimizzato solo in un array di numeri.

Un altro motivo per cui un array deve essere forzato nel formato ottimizzato può essere se si tenta di accedere a un elemento al di fuori dei limiti dell'array, come discusso nella presentazione.

Il primo ciclo con <=tenta di leggere un elemento oltre la fine dell'array. L'algoritmo funziona ancora correttamente, poiché nell'ultima iterazione aggiuntiva:

  • this.primes[i]valuta undefinedperché iè oltre la fine dell'array.
  • candidate % undefined(per qualsiasi valore di candidate) valuta NaN.
  • NaN == 0valuta false.
  • Pertanto, return truenon viene eseguito.

Quindi è come se l'iterazione extra non fosse mai avvenuta - non ha alcun effetto sul resto della logica. Il codice produce lo stesso risultato che avrebbe senza l'iterazione aggiuntiva.

Ma per arrivarci, ha provato a leggere un elemento inesistente oltre la fine dell'array. Questo costringe l'array fuori dall'ottimizzazione - o almeno lo ha fatto al momento di questo discorso.

Il secondo ciclo con < legge solo gli elementi presenti nell'array, quindi consente un array e un codice ottimizzati.

Il problema è descritto nelle pagine 90-91 del discorso, con relativa discussione nelle pagine prima e dopo.

Mi è capitato di partecipare a questa presentazione I / O di Google e successivamente ho parlato con l'oratore (uno degli autori del V8). Avevo usato una tecnica nel mio codice che prevedeva la lettura oltre la fine di un array come tentativo fuorviato (a ben vedere) di ottimizzare una particolare situazione. Ha confermato che se si provasse a leggere anche oltre la fine di un array, si impedirebbe l'utilizzo del formato ottimizzato semplice.

Se ciò che l'autore di V8 ha detto è ancora vero, la lettura oltre la fine dell'array ne impedirebbe l'ottimizzazione e dovrebbe ricadere nel formato più lento.

Ora è possibile che V8 sia stato migliorato nel frattempo per gestire efficacemente questo caso o che altri motori JavaScript lo gestiscano in modo diverso. Non ne so in un modo o nell'altro, ma questa deottimizzazione è ciò di cui parlava la presentazione.


1
Sono abbastanza sicuro che l'array sia ancora contiguo: non c'è motivo di cambiare il layout della memoria. Ciò che conta, tuttavia, è che il controllo degli indici fuori limite nell'accesso alla proprietà non può essere ottimizzato e il codice a volte viene inserito undefinedinvece di un numero che porta a un calcolo diverso.
Bergi,

1
@Bergi Non sono un esperto di JS / V8, ma gli oggetti nei linguaggi GC sono quasi sempre riferimenti agli oggetti reali. Questi oggetti reali hanno un'allocazione indipendente, anche se i riferimenti sono contigui, poiché la durata dell'oggetto GC non è legata. Gli ottimizzatori possono impacchettare tali allocazioni indipendenti in modo che siano adiacenti, ma (a) la memoria usa i razzi e (b) hai due blocchi contigui su cui si ripetono (i riferimenti e i dati a cui fa riferimento) anziché uno. Suppongo che un pazzo ottimizzatore potrebbe intercalare i riferimenti e i dati a cui fa riferimento e avere un array che possiede strisce di memoria ...
Yakk - Adam Nevraumont

1
@Bergi L'array potrebbe essere contiguo nel caso non ottimizzato, ma gli elementi dell'array non sono dello stesso tipo del caso ottimizzato. La versione ottimizzata è una semplice serie di numeri senza lanugine aggiuntiva. La versione non ottimizzata è una matrice di oggetti (un formato di oggetto interno, non JavaScript Object), poiché deve supportare qualsiasi combinazione di tipi di dati nella matrice. Come ho accennato in precedenza, il codice nel loop che viene alimentato undefinednon influisce sulla correttezza dell'algoritmo, non modifica affatto il calcolo (è come se l'iterazione aggiuntiva non fosse mai avvenuta).
Michael Geary,

3
@Bergi L'autore V8 che ha tenuto questo discorso ha detto che il tentativo di leggere al di fuori dei limiti dell'array fa sì che l'array venga trattato come se avesse un mix di tipi: invece del formato ottimizzato solo numerico, de-ottimizza l'array di nuovo su il formato generico. Nel caso ottimizzato si tratta di un semplice array di numeri come si potrebbe usare in un programma C. Nel caso de-ottimizzato si tratta di una matrice di Valueoggetti che può contenere riferimenti a valori di qualsiasi tipo. (Ho inventato il nome Value, ma il punto è che gli elementi dell'array non sono solo numeri semplici ma sono oggetti che racchiudono numeri o altri tipi.)
Michael Geary,

3
Lavoro su V8. L'array in questione verrebbe contrassegnato come HOLEYperché è stato creato usando new Array(n)(sebbene questa parte del codice non fosse visibile nell'OP). HOLEYle matrici rimangono HOLEYper sempre nel V8 , anche quando successivamente vengono riempite. Detto questo, l'array essendo bucato non è la ragione del problema perf in questo caso; significa solo che dobbiamo fare un controllo Smi extra su ogni iterazione (per proteggerci dai buchi), il che non è un grosso problema.
Mathias Bynens,

19

TL; DR Il loop più lento è dovuto all'accesso ai "fuori campo" dell'array, che costringe il motore a ricompilare la funzione con ottimizzazioni minori o addirittura assenti O a non compilare la funzione con una di queste ottimizzazioni per iniziare ( se il compilatore (JIT-) ha rilevato / sospettato questa condizione prima della prima 'versione' della compilation), continua a leggere perché;


Qualcuno deve solo dirlo (assolutamente stupito nessuno lo ha già fatto):
c'era un tempo in cui lo snippet dell'OP sarebbe stato di fatto un esempio in un libro di programmazione per principianti destinato a delineare / enfatizzare che gli 'array' in javascript sono indicizzati a partire da a 0, non 1, e come tale essere usato come esempio di un "errore per principianti" comune (non ti piace come ho evitato la frase "errore di programmazione" ;)): accesso all'array fuori limite .

Esempio 1:
a Dense Array(essendo contigui (significa senza vuoti tra gli indici) E in realtà un elemento in ciascun indice) di 5 elementi usando l'indicizzazione basata su 0 (sempre in ES262).

var arr_five_char=['a', 'b', 'c', 'd', 'e']; // arr_five_char.length === 5
//  indexes are:    0 ,  1 ,  2 ,  3 ,  4    // there is NO index number 5



Quindi non stiamo davvero parlando della differenza di prestazioni tra <vs <=(o "un'iterazione extra"), ma stiamo parlando:
"perché lo snippet corretto (b) viene eseguito più velocemente dello snippet errato (a)"?

La risposta è duplice (anche se dal punto di vista dell'implementazione del linguaggio ES262 entrambe sono forme di ottimizzazione):

  1. Rappresentazione dei dati: come rappresentare / archiviare l'array internamente in memoria (oggetto, hashmap, array numerico "reale", ecc.)
  2. Codice macchina funzionale: come compilare il codice che accede / gestisce (legge / modifica) questi "array"

L'articolo 1 è sufficientemente (e correttamente IMHO) spiegato dalla risposta accettata , ma ciò spende solo 2 parole ("il codice") nell'articolo 2: compilazione .

Più precisamente: JIT-Compilation e ancor più importante JIT- RE- Compilation!

Le specifiche del linguaggio sono fondamentalmente solo una descrizione di una serie di algoritmi ("passaggi da eseguire per ottenere un risultato finale definito"). Che, a quanto pare, è un modo molto bello per descrivere una lingua. E lascia il metodo effettivo che un motore utilizza per ottenere risultati specifici aperto agli implementatori, offrendo ampie opportunità di trovare modi più efficienti per produrre risultati definiti. Un motore conforme alle specifiche dovrebbe fornire risultati conformi alle specifiche per qualsiasi input definito.

Ora, con il codice javascript / librerie / utilizzo in aumento e ricordando quante risorse (tempo / memoria / ecc.) Utilizza un compilatore "reale", è chiaro che non possiamo far aspettare così tanto gli utenti che visitano una pagina Web (e li richiedono avere tante risorse disponibili).

Immagina la seguente semplice funzione:

function sum(arr){
  var r=0, i=0;
  for(;i<arr.length;) r+=arr[i++];
  return r;
}

Perfettamente chiaro, vero? Non richiede NESSUN chiarimento aggiuntivo, giusto? Il tipo di ritorno è Number, giusto?
Bene .. no, no & no ... Dipende dall'argomento che passi al parametro della funzione denominata arr...

sum('abcde');   // String('0abcde')
sum([1,2,3]);   // Number(6)
sum([1,,3]);    // Number(NaN)
sum(['1',,3]);  // String('01undefined3')
sum([1,,'3']);  // String('NaN3')
sum([1,2,{valueOf:function(){return this.val}, val:6}]);  // Number(9)
var val=5; sum([1,2,{valueOf:function(){return val}}]);   // Number(8)

Vedi il problema? Quindi considera che questo sta raschiando a malapena le enormi possibili permutazioni ... Non sappiamo nemmeno che tipo di TIPO la funzione TORNA finché non abbiamo finito ...

Ora immagina che questo stesso codice funzione sia effettivamente utilizzato su diversi tipi o persino variazioni di input, entrambi descritti letteralmente (nel codice sorgente) e generati dinamicamente "array" nel programma.

Pertanto, se si dovesse compilare la funzione sumSOLO UNA VOLTA, l'unico modo che restituisce sempre il risultato definito dalle specifiche per qualsiasi e tutti i tipi di input, ovviamente, solo eseguendo TUTTI i passaggi E AND secondari prescritti dalle specifiche è possibile garantire risultati conformi alle specifiche (come un browser pre-y2k senza nome). Non ci sono ottimizzazioni (perché nessuna ipotesi) e il linguaggio di script interpretazione lenta è morto.

JIT-Compilation (JIT come in Just In Time) è l'attuale soluzione popolare.

Quindi, inizi a compilare la funzione usando i presupposti su ciò che fa, restituisce e accetta.
viene fornito un controllo il più semplice possibile per rilevare se la funzione potrebbe iniziare a restituire risultati non conformi alle specifiche (ad esempio perché riceve input imprevisti). Quindi, getta via il risultato compilato precedente e ricompila in qualcosa di più elaborato, decidi cosa fare con il risultato parziale che hai già (è valido fidarti o calcola di nuovo per essere sicuro), ricollega la funzione al programma e riprova. Alla fine, ricadendo sull'interpretazione graduale dello script come nelle specifiche.

Tutto ciò richiede tempo!

Tutti i browser funzionano sui loro motori, per ogni sub-versione vedrai che le cose miglioreranno e regrediranno. Le stringhe erano ad un certo punto della storia stringhe davvero immutabili (quindi array.join era più veloce della concatenazione di stringhe), ora usiamo le corde (o simili) che alleviano il problema. Entrambi restituiscono risultati conformi alle specifiche ed è quello che conta!

Per farla breve: solo perché la semantica del linguaggio javascript spesso ci ha restituito le spalle (come con questo bug silenzioso nell'esempio del PO) non significa che errori "stupidi" aumentino le nostre possibilità che il compilatore sputi codice macchina veloce. Presuppone che abbiamo scritto le istruzioni "di solito" corrette: l'attuale mantra che noi "utenti" (del linguaggio di programmazione) deve avere è: aiutare il compilatore, descrivere ciò che vogliamo, favorire idiomi comuni (prendere suggerimenti da asm.js per la comprensione di base quali browser possono provare a ottimizzare e perché).

Per questo motivo , parlare delle prestazioni è importante, MA ANCHE un campo minato (e a causa di tale campo minato voglio davvero finire con il puntare (e citare) del materiale pertinente:

L'accesso alle proprietà degli oggetti inesistenti e agli elementi dell'array fuori dai limiti restituisce il undefinedvalore invece di sollevare un'eccezione. Queste caratteristiche dinamiche rendono conveniente la programmazione in JavaScript, ma rendono anche difficile la compilazione di JavaScript in un codice macchina efficiente.

...

Una premessa importante per un'ottimizzazione JIT efficace è che i programmatori utilizzano le funzionalità dinamiche di JavaScript in modo sistematico. Ad esempio, i compilatori JIT sfruttano il fatto che le proprietà degli oggetti vengono spesso aggiunte a un oggetto di un determinato tipo in un ordine specifico o che raramente si verificano accessi di array fuori limite. I compilatori JIT sfruttano questi presupposti di regolarità per generare un codice macchina efficiente in fase di esecuzione. Se un blocco di codice soddisfa i presupposti, il motore JavaScript esegue un codice macchina generato e efficiente. Altrimenti, il motore deve tornare al codice più lento o all'interpretazione del programma.

Fonte:
"JITProf: Individuazione del codice JavaScript non compatibile con JIT"
Pubblicazione Berkeley, 2014, di Liang Gong, Michael Pradel, Koushik Sen.
http://software-lab.org/publications/jitprof_tr_aug3_2014.pdf

ASM.JS (inoltre non piace l'accesso fuori array fuori limite):

Compilazione anticipata

Poiché asm.js è un sottoinsieme rigoroso di JavaScript, questa specifica definisce solo la logica di convalida: la semantica dell'esecuzione è semplicemente quella di JavaScript. Tuttavia, asm.js convalidato è suscettibile di compilazione anticipata (AOT). Inoltre, il codice generato da un compilatore AOT può essere abbastanza efficiente, caratterizzato da:

  • rappresentazioni unboxed di numeri interi e numeri in virgola mobile;
  • assenza di controlli del tipo di runtime;
  • assenza di raccolta rifiuti; e
  • carichi e negozi di heap efficienti (con strategie di implementazione che variano in base alla piattaforma).

Il codice che non riesce a convalidare deve tornare all'esecuzione con mezzi tradizionali, ad es. Interpretazione e / o compilazione just-in-time (JIT).

http://asmjs.org/spec/latest/

e infine https://blogs.windows.com/msedgedev/2015/05/07/bringing-asm-js-to-chakra-microsoft-edge/
dove c'è una piccola sottosezione sui miglioramenti delle prestazioni interne del motore quando si rimuovono i limiti- check (pur alzando il limite-check al di fuori del loop aveva già un miglioramento del 40%).



EDIT:
nota che più fonti parlano di diversi livelli di JIT-Compompilation fino all'interpretazione.

Esempio teorico basato sulle informazioni di cui sopra, relativamente allo snippet del PO:

  • Chiama per isPrimeDivisible
  • Compilare isPrimeDivisible utilizzando ipotesi generali (come nessun accesso fuori limite)
  • Lavora
  • BAM, improvvisamente la matrice accede fuori dai limiti (proprio alla fine).
  • Merda, dice engine, ricompiliamo isPrimeDivisible usando diversi (meno) presupposti, e questo motore di esempio non cerca di capire se può riutilizzare il risultato parziale corrente, quindi
  • Ricalcola tutto il lavoro usando la funzione più lenta (si spera che finisca, altrimenti ripeti e questa volta basta interpretare il codice).
  • Risultato di ritorno

Quindi il tempo era:
prima esecuzione (fallito alla fine) + facendo tutto di nuovo tutto da capo usando un codice macchina più lento per ogni iterazione + la ricompilazione ecc. Chiaramente impiega> 2 volte di più in questo esempio teorico !



EDIT 2: (disclaimer: congettura basata sui fatti di seguito)
Più ci penso, più penso che questa risposta possa effettivamente spiegare la ragione più dominante di questa "penalità" su frammenti errati a (o bonus di prestazioni su frammenti b , a seconda di come lo pensi), proprio perché sono adorato nel chiamarlo (frammento a) un errore di programmazione:

È abbastanza allettante supporre che si this.primestratti di un puro numerico "denso array"

  • Letterale hardcoded nel codice sorgente (noto candidato eccellente per diventare un array "reale" poiché tutto è già noto al compilatore prima del tempo di compilazione) OPPURE
  • molto probabilmente generato usando una funzione numerica che riempie un pre-dimensionato ( new Array(/*size value*/)) in ordine sequenziale crescente (un altro candidato noto da molto tempo a diventare un array 'reale').

Sappiamo anche che la primeslunghezza dell'array è memorizzata nella cache come prime_count! (indicando l'intento e la dimensione fissa).

Sappiamo anche che la maggior parte dei motori inizialmente passa gli array come copia su modifica (quando necessario), il che rende la loro gestione molto più veloce (se non li cambi).

È quindi ragionevole presumere che l'array primessia molto probabilmente già un array ottimizzato internamente che non viene modificato dopo la creazione (semplice da sapere per il compilatore se non esiste un codice che modifica l'array dopo la creazione) e quindi è già (se applicabile a il motore) memorizzato in modo ottimizzato, praticamente come se fosse un Typed Array.

Come ho cercato di chiarire con il mio sumesempio di funzione, gli argomenti che vengono superati influenzano fortemente ciò che deve realmente accadere e come tale viene compilato quel particolare codice in codice macchina. Passare Stringa alla sumfunzione non dovrebbe cambiare la stringa ma cambiare il modo in cui la funzione è compilata da JIT! Passare un array a sumdovrebbe compilare una versione diversa (forse anche aggiuntiva per questo tipo, o 'forma' come lo chiamano, dell'oggetto che è stato passato) di codice macchina.

Come sembra leggermente bonkus convertire l'array di tipo Typed_Array primesal volo in qualcosa_else mentre il compilatore sa che questa funzione non lo modificherà nemmeno!

Sotto questi presupposti che lascia 2 opzioni:

  1. Compilare come numero-cruncher assumendo che non ci siano limiti al di fuori dei limiti, incorrere in un problema al di fuori dei limiti alla fine, ricompilare e ripetere il lavoro (come indicato nell'esempio teorico nella modifica 1 sopra)
  2. Il compilatore ha già rilevato (o sospettato?) Un accesso non previsto in anticipo e la funzione era compilata JIT come se l'argomento passato fosse un oggetto rado con conseguente rallentamento del codice macchina funzionale (poiché avrebbe più controlli / conversioni / coercizioni eccetera.). In altre parole: la funzione non è mai stata ammissibile per determinate ottimizzazioni, è stata compilata come se ricevesse un argomento "sparse array" (- like).

Ora mi chiedo davvero quale di questi 2 sia!


2
Una buona discussione su alcune delle questioni sottostanti - tuttavia spieghi a malapena la risposta (nell'ultima frase). Magari aggiungi un tl; dr in cima? ad es. "Il ciclo più lento è dovuto al superamento della matrice dei limiti, che forza il motore a rivalutare il ciclo senza ottimizzazioni. Continua a leggere per scoprire perché."
brichins,

@brichins: grazie e grazie per il suggerimento, che ho riformulato un po 'alla luce della mia seconda modifica aggiuntiva, perché più ci penso ora, anche quella frase in alto sembra effettivamente corretta
GitaarLAB

6

Per aggiungere un po 'di scientificità ad esso, ecco un jsperf

https://jsperf.com/ints-values-in-out-of-array-bounds

Mette alla prova il caso di controllo di un array riempito con ints e loop facendo l'aritmetica modulare rimanendo all'interno dei limiti. Ha 5 casi di test:

  • 1. Ciclo continuo fuori dai limiti
  • 2. Matrici bucate
  • 3. Aritmetica modulare contro NaNs
  • 4. Valori completamente indefiniti
  • 5. Utilizzo di a new Array()

Mostra che i primi 4 casi sono davvero dannosi per le prestazioni. Il loop out dei limiti è leggermente migliore rispetto agli altri 3, ma tutti e 4 sono circa il 98% più lenti rispetto al caso migliore.
Il new Array()case è buono quasi quanto l'array raw, solo un po 'più lento.

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.