perché quest'ultima funzione è più veloce del 10% anche se deve creare più volte le variabili?


14
var toSizeString = (function() {

 var KB = 1024.0,
     MB = 1024 * KB,
     GB = 1024 * MB;

  return function(size) {
    var gbSize = size / GB,
        gbMod  = size % GB,
        mbSize = gbMod / MB,
        mbMod  = gbMod % MB,
        kbSize = mbMod / KB;

    if (Math.floor(gbSize)) {
      return gbSize.toFixed(1) + 'GB';
    } else if (Math.floor(mbSize)) {
      return mbSize.toFixed(1) + 'MB';
    } else if (Math.floor(kbSize)) {
      return kbSize.toFixed(1) + 'KB';
    } else {
      return size + 'B';
    }
  };
})();

E la funzione più veloce: (nota che deve sempre calcolare le stesse variabili kb / mb / gb più e più volte). Dove guadagna prestazioni?

function toSizeString (size) {

 var KB = 1024.0,
     MB = 1024 * KB,
     GB = 1024 * MB;

 var gbSize = size / GB,
     gbMod  = size % GB,
     mbSize = gbMod / MB,
     mbMod  = gbMod % MB,
     kbSize = mbMod / KB;

 if (Math.floor(gbSize)) {
      return gbSize.toFixed(1) + 'GB';
 } else if (Math.floor(mbSize)) {
      return mbSize.toFixed(1) + 'MB';
 } else if (Math.floor(kbSize)) {
      return kbSize.toFixed(1) + 'KB';
 } else {
      return size + 'B';
 }
};

3
In qualsiasi linguaggio tipizzato staticamente le "variabili" verrebbero compilate come costanti. Forse i moderni motori JS sono in grado di fare la stessa ottimizzazione. Questo sembra non funzionare se le variabili fanno parte di una chiusura.
usr

6
questo è un dettaglio di implementazione del motore JavaScript che stai utilizzando. Il tempo e lo spazio teorici sono gli stessi, è solo l'implementazione di un determinato motore JavaScript che può variare. Quindi, per rispondere correttamente alla tua domanda, devi elencare il motore JavaScript specifico con cui le hai misurate. Forse qualcuno conosce i dettagli della sua implementazione per dire come / perché ha reso uno più ottimale dell'altro. Inoltre, è necessario pubblicare il codice di misurazione.
Jimmy Hoffa,

si usa la parola "calcolo" in riferimento a valori costanti; non c'è davvero nulla da calcolare lì in ciò a cui ti riferisci. L'aritmetica dei valori costanti è una delle ottimizzazioni più semplici e ovvie che i compilatori fanno, quindi ogni volta che vedi un'espressione che ha solo valori costanti, puoi semplicemente supporre che l'intera espressione sia ottimizzata su un singolo valore costante.
Jimmy Hoffa,

@JimmyHoffa è vero, ma d'altra parte deve creare 3 variabili costanti per ogni chiamata di funzione ...
Tomy

Le costanti di @Tomy non sono variabili. Non variano, quindi non è necessario ricrearli dopo la compilazione. Una costante viene generalmente collocata nella memoria e ogni portata futura per quella costante viene indirizzata nello stesso identico posto, non è necessario ricrearla perché il suo valore non varierà mai , quindi non è una variabile. I compilatori generalmente non emettono codice che crea costanti, il compilatore esegue la creazione e indirizza tutti i riferimenti di codice a ciò che ha creato.
Jimmy Hoffa,

Risposte:


23

I moderni motori JavaScript eseguono tutti compilazioni just-in-time. Non puoi fare alcuna presunzione su ciò che "deve creare più e più volte". Questo tipo di calcolo è relativamente facile da ottimizzare, in entrambi i casi.

D'altra parte, chiudere su variabili costanti non è un caso tipico per cui si dovrebbe scegliere la compilazione JIT. In genere si crea una chiusura quando si desidera essere in grado di modificare tali variabili su invocazioni diverse. Stai anche creando una dereference puntatore aggiuntiva per accedere a quelle variabili, come la differenza tra l'accesso a una variabile membro e un int locale in OOP.

Questo tipo di situazione è il motivo per cui le persone eliminano la linea di "ottimizzazione prematura". Le facili ottimizzazioni sono già state eseguite dal compilatore.


Ho il sospetto che sia l'attraversamento dell'ambito per la risoluzione variabile che sta causando la perdita, come dici tu. Sembra ragionevole, ma chissà quale follia risiede in un motore JIT JavaScript ...
Jimmy Hoffa

1
Possibile espansione di questa risposta: il motivo per cui un JIT ignora un'ottimizzazione facile per un compilatore offline è perché le prestazioni dell'intero compilatore sono più importanti che in casi insoliti.
Leushenko,

12

Le variabili sono economiche. I contesti di esecuzione e le catene di portata sono costosi.

Esistono varie risposte che si riducono essenzialmente a "perché le chiusure", e quelle sono essenzialmente vere, ma il problema non è specificamente con la chiusura, è il fatto che hai una funzione che fa riferimento a variabili in un ambito diverso. Avresti lo stesso problema se queste fossero variabili globali suwindow sull'oggetto, al contrario delle variabili locali all'interno dell'IIFE. Provalo e vedi.

Quindi nella tua prima funzione, quando il motore vede questa affermazione:

var gbSize = size / GB;

Deve seguire i seguenti passi:

  1. Cerca una variabile sizenell'ambito corrente. (Trovato.)
  2. Cerca una variabile GBnell'ambito corrente. (Non trovato.)
  3. Cerca una variabile GBnell'ambito genitore. (Trovato.)
  4. Fai il calcolo e assegna a gbSize.

Il passaggio 3 è notevolmente più costoso della semplice allocazione di una variabile. Inoltre, lo fai cinque volte , anche due volte per entrambi GBe MB. Ho il sospetto che se li hai allettati all'inizio della funzione (ad esvar gb = GB ) E invece riferimento all'alias, si produrrebbe effettivamente un piccolo aumento di velocità, anche se è anche possibile che alcuni motori JS eseguano già questa ottimizzazione. E, naturalmente, il modo più efficace per accelerare l'esecuzione è semplicemente quello di non attraversare affatto la catena dell'ambito.

Tieni presente che JavaScript non è come un linguaggio compilato e tipicamente statico in cui il compilatore risolve questi indirizzi variabili al momento della compilazione. Il motore JS deve risolverli per nome e queste ricerche avvengono in fase di esecuzione, ogni volta. Quindi vuoi evitarli quando possibile.

L'assegnazione delle variabili è estremamente economica in JavaScript. In realtà potrebbe essere l'operazione più economica, anche se non ho nulla per il backup di tale affermazione. Tuttavia, è sicuro affermare che non è quasi mai una buona idea cercare di evitare di creare variabili; quasi ogni ottimizzazione che proverai a fare in quell'area finirà per peggiorare le cose, dal punto di vista delle prestazioni.


E anche se il "ottimizzazione" non influisce sulle prestazioni negativamente, quasi certamente sta andando ad incidere negativamente la leggibilità del codice. Il che, a meno che tu non stia facendo cose folli computazionali, è spesso un cattivo compromesso da fare (apparentemente nessuna ancora permalink; cerca "2009-02-17 11:41"). Come dice il riassunto: "Scegli la chiarezza sulla velocità, se la velocità non è assolutamente necessaria".
un CVn del

Anche quando si scrive un interprete di base per i linguaggi dinamici, l'accesso variabile durante il runtime tende ad essere un'operazione O (1), e l'attraversamento dell'ambito O (n) non è nemmeno necessario durante la compilazione iniziale. In ogni ambito, a ciascuna variabile appena dichiarata viene assegnato un numero, quindi dato var a, b, cche possiamo accedere ba scope[1]. Tutti gli ambiti sono numerati e se questo ambito è nidificato a cinque ambiti in profondità, bviene completamente risolto da ciò env[5][1]che è noto durante l'analisi. Nel codice nativo, gli ambiti corrispondono ai segmenti dello stack. Le chiusure sono più complicate dal momento che devono eseguire il backup e sostituire ilenv
amon

@amon: Che potrebbe essere come avresti idealmente come farlo funzionare, ma non è come funziona realmente. Persone molto più competenti ed esperte di quanto io abbia scritto libri su questo; in particolare ti consiglierei JavaScript ad alte prestazioni di Nicholas C. Zakas. Ecco uno snippet e ha anche parlato con i benchmark per eseguirne il backup. Ovviamente non è certamente l'unico, solo il più noto. JavaScript ha un ambito lessicale, quindi in realtà le chiusure non sono poi così speciali - in sostanza, tutto è una chiusura.
Aaronaught,

@Aaronaught Interessante. Dato che quel libro ha 5 anni, ero interessato al modo in cui un motore JS attuale gestisce le ricerche variabili e osservava il backend x64 del motore V8. Durante l'analisi statica, la maggior parte delle variabili viene risolta staticamente e gli viene assegnato un offset di memoria nel loro ambito. Gli ambiti delle funzioni sono rappresentati come elenchi collegati e l'assembly viene emesso come un ciclo non srotolato per raggiungere l'ambito corretto. Qui otterremmo l'equivalente del codice C *(scope->outer + variable_offset)per un accesso; ogni livello di ambito di funzione extra costa una dereference puntatore aggiuntiva. Sembra che avessimo ragione entrambi :)
amon

2

Un esempio comporta una chiusura, l'altro no. L'implementazione delle chiusure è piuttosto complicata, dal momento che le variabili chiuse non funzionano come le normali variabili. Questo è più ovvio in un linguaggio di basso livello come C, ma userò JavaScript per illustrarlo.

Una chiusura non consiste solo di una funzione, ma anche di tutte le variabili su cui ha chiuso. Quando vogliamo invocare tale funzione, dobbiamo anche fornire tutte le variabili chiuse. Possiamo modellare una chiusura mediante una funzione che riceve un oggetto come primo argomento che rappresenta queste variabili chiuse:

function add(vars, y) {
  vars.x += y;
}

function getSum(vars) {
  return vars.x;
}

function makeAdder(x) {
  return { x: x, add: add, getSum: getSum };
}

var adder = makeAdder(40);
adder.add(adder, 2);
console.log(adder.getSum(adder));  //=> 42

Nota la convenzione di chiamata imbarazzante che closure.apply(closure, ...realArgs)richiede

Il supporto per gli oggetti incorporati di JavaScript consente di omettere l' varsargomento esplicito e ci consente thisinvece di utilizzare :

function add(y) {
  this.x += y;
}

function getSum() {
  return this.x;
}

function makeAdder(x) {
  return { x: x, add: add, getSum: getSum };
}

var adder = makeAdder(40);
adder.add(2);
console.log(adder.getSum());  //=> 42

Questi esempi sono equivalenti a questo codice usando effettivamente le chiusure:

function makeAdder(x) {
  return {
    add: function (y) { x += y },
    getSum: function () { return x },
  };
}

var adder = makeAdder(40);
adder.add(2);
console.log(adder.getSum());  //=> 42

In quest'ultimo esempio, l'oggetto viene utilizzato solo per raggruppare le due funzioni restituite; l' thisassociazione è irrilevante. Tutti i dettagli su come rendere possibili le chiusure - passando i dati nascosti alla funzione reale, cambiando tutti gli accessi alle variabili di chiusura alle ricerche in quei dati nascosti - sono curati dalla lingua.

Ma chiamare le chiusure comporta l'overhead del passaggio di quei dati extra, e l'esecuzione di una chiusura comporta l'overhead delle ricerche in quei dati extra - aggravati dalla cattiva localizzazione della cache e di solito una dereferenza del puntatore rispetto alle variabili ordinarie - quindi non sorprende che una soluzione che non si basa su chiusure funziona meglio. Soprattutto perché tutto ciò che la tua chiusura ti salva da fare sono alcune operazioni aritmetiche estremamente economiche, che potrebbero anche essere piegate costantemente durante l'analisi.

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.