2010-08-28 4 views
6

Ho svolto ricerche sulle prestazioni di JavaScript. Ho imparato che quando si accede a più di una volta, di solito è meglio copiare le variabili di chiusura e i membri della classe in ambito locale per accelerare le cose. Ad esempio:Quando si perfezionano le prestazioni, qual è il modo migliore per chiamare più volte i metodi JavaScript?

var i = 100; 
var doSomething = function() { 
    var localI = i; 
    // do something with localI a bunch of times 

    var obj = { 
     a: 100 
    }; 
    var objA = obj.a; 
    // do something with objA a bunch of times 
}; 

Capisco questo; aggiunge una scorciatoia per l'interprete che cerca il valore per nome. Questo concetto diventa molto chiaro quando si tratta di metodi. All'inizio pensavo che avrebbe funzionato allo stesso modo. Ad esempio:

var obj = { 
    fn: function() { 
     // Do something 
     return this.value; 
    }, 
    value: 100 
}; 
var objFn = obj.fn 
objFn(); 
// call objFn a bunch of times 

Così com'è, questo non funzionerà affatto. L'accesso al metodo come questo lo rimuove dal suo ambito. Quando raggiunge la riga this.value, questo si riferisce all'oggetto window e this.value sarà probabilmente indefinito. Invece di chiamare direttamente objFn e perdere scope, potrei riportare il suo scope in esso con objFn.call (obj) ma questo funziona meglio o peggio dell'originale obj.fn()?

Ho deciso di scrivere una sceneggiatura per testarlo e ho ottenuto risultati molto confusi. Questo script esegue iterazioni su diversi test che effettuano più chiamate in loop nella funzione sopra descritta. Il tempo medio impiegato per ciascun test viene inviato al corpo.

Un oggetto viene creato con molti metodi semplici su di esso. I metodi extra ci sono per determinare se l'interprete deve lavorare molto più difficile per individuare un metodo specifico.

Test 1 chiama semplicemente this.a();
Test 2 crea una variabile locale a = this.a quindi chiama a.call (this);
Test 3 crea una variabile locale utilizzando la funzione di collegamento di YUI per preservare l'ambito. Ho commentato questo. Le chiamate alle funzioni extra create da YUI rendono questo modo più lento.

I test 4, 5 e 6 sono copie di 1, 2, 3 tranne che per z anziché a.

La funzione successiva di YUI viene utilizzata per prevenire errori di script in fuga. I tempi vengono eseguiti con i metodi di test effettivi, quindi il setTimeouts non dovrebbe influenzare i risultati. Ogni funzione è chiamata un totale di 10000000 volte. (Facilmente configurabile se si desidera eseguire test.)

Ecco il mio intero documento XHTML che ho usato per testare.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xml:lang="en" dir="ltr"> 
    <head> 
     <script type="text/javascript" src="http://yui.yahooapis.com/combo?3.1.2/build/yui/yui-min.js"></script> 
     <script> 
      YUI().use('node', function (Y) { 
       var o = { 
        value: '', 
        a: function() { 
         this.value += 'a'; 
        }, 
        b: function() { 
         this.value += 'b'; 
        }, 
        c: function() { 
         this.value += 'c'; 
        }, 
        d: function() { 
         this.value += 'd'; 
        }, 
        e: function() { 
         this.value += 'e'; 
        }, 
        f: function() { 
         this.value += 'f'; 
        }, 
        g: function() { 
         this.value += 'g'; 
        }, 
        h: function() { 
         this.value += 'h'; 
        }, 
        i: function() { 
         this.value += 'i'; 
        }, 
        j: function() { 
         this.value += 'j'; 
        }, 
        k: function() { 
         this.value += 'k'; 
        }, 
        l: function() { 
         this.value += 'l'; 
        }, 
        m: function() { 
         this.value += 'm'; 
        }, 
        n: function() { 
         this.value += 'n'; 
        }, 
        o: function() { 
         this.value += 'o'; 
        }, 
        p: function() { 
         this.value += 'p'; 
        }, 
        q: function() { 
         this.value += 'q'; 
        }, 
        r: function() { 
         this.value += 'r'; 
        }, 
        s: function() { 
         this.value += 's'; 
        }, 
        t: function() { 
         this.value += 't'; 
        }, 
        u: function() { 
         this.value += 'u'; 
        }, 
        v: function() { 
         this.value += 'v'; 
        }, 
        w: function() { 
         this.value += 'w'; 
        }, 
        x: function() { 
         this.value += 'x'; 
        }, 
        y: function() { 
         this.value += 'y'; 
        }, 
        z: function() { 
         this.value += 'z'; 
        }, 
        reset: function() { 
         this.value = ''; 
        }, 
        test1: function (length) { 
         var time = new Date().getTime(); 

         while ((length -= 1)) { 
          this.a(); 
         } 
         return new Date().getTime() - time; 
        }, 
        test2: function (length) { 
         var a = this.a, 
         time = new Date().getTime(); 

         while ((length -= 1)) { 
          a.call(this); 
         } 
         return new Date().getTime() - time; 
        }, 
        test3: function (length) { 
         var a = Y.bind(this.a, this), 
         time = new Date().getTime(); 

         while ((length -= 1)) { 
          a(); 
         } 
         return new Date().getTime() - time; 
        }, 
        test4: function (length) { 
         var time = new Date().getTime(); 

         while ((length -= 1)) { 
          this.z(); 
         } 
         return new Date().getTime() - time; 
        }, 
        test5: function (length) { 
         var z = this.z, 
         time = new Date().getTime(); 

         while ((length -= 1)) { 
          z.call(this); 
         } 
         return new Date().getTime() - time; 
        }, 
        test6: function (length) { 
         var z = Y.bind(this.z, this), 
         time = new Date().getTime(); 

         while ((length -= 1)) { 
          z(); 
         } 
         return new Date().getTime() - time; 
        } 
       }, 
       iterations = 100, iteration = iterations, length = 100000, 
       t1 = 0, t2 = 0, t3 = 0, t4 = 0, t5 = 0, t6 = 0, body = Y.one('body'); 

       body.set('innerHTML', '<span>Running ' + iterations + ' Iterations&hellip;</span>'); 
       while ((iteration -= 1)) { 
        Y.later(1, null, function (iteration) { 
         Y.later(1, null, function() { 
          o.reset(); 
          t1 += o.test1(length); 
         }); 
         Y.later(1, null, function() { 
          o.reset(); 
          t2 += o.test2(length); 
         }); 
         /*Y.later(1, null, function() { 
          o.reset(); 
          t3 += o.test3(length); 
         });*/ 
         Y.later(1, null, function() { 
          o.reset(); 
          t4 += o.test4(length); 
         }); 
         Y.later(1, null, function() { 
          o.reset(); 
          t5 += o.test5(length); 
         }); 
         /*Y.later(1, null, function() { 
          o.reset(); 
          t6 += o.test6(length); 
         });*/ 
         if (iteration === 1) { 
          Y.later(10, null, function() { 
           t1 /= iterations; 
           t2 /= iterations; 
           //t3 /= iterations; 
           t4 /= iterations; 
           t5 /= iterations; 
           //t6 /= iterations; 

           //body.set('innerHTML', '<dl><dt>Test 1: this.a();</dt><dd>' + t1 + '</dd><dt>Test 2: a.call(this);</dt><dd>' + t2 + '</dd><dt>Test 3: a();</dt><dd>' + t3 + '</dd><dt>Test 4: this.z();</dt><dd>' + t4 + '</dd><dt>Test 5: z.call(this);</dt><dd>' + t5 + '</dd><dt>Test 6: z();</dt><dd>' + t6 + '</dd></dl>'); 
           body.set('innerHTML', '<dl><dt>Test 1: this.a();</dt><dd>' + t1 + '</dd><dt>Test 2: a.call(this);</dt><dd>' + t2 + '</dd><dt>Test 4: this.z();</dt><dd>' + t4 + '</dd><dt>Test 5: z.call(this);</dt><dd>' + t5 + '</dd></dl>'); 
          }); 
         } 
        }, iteration); 
       } 
      }); 
     </script> 
    </head> 
    <body> 
    </body> 
</html> 

Ho eseguito questo utilizzando Windows 7 in tre diversi browser. Questi risultati sono in millisecondi.

Firefox 3.6.8

Test 1: this.a(); 
    9.23 
Test 2: a.call(this); 
    9.67 
Test 4: this.z(); 
    9.2 
Test 5: z.call(this); 
    9.61 

Chrome 7.0.503.0

Test 1: this.a(); 
    5.25 
Test 2: a.call(this); 
    4.66 
Test 4: this.z(); 
    3.71 
Test 5: z.call(this); 
    4.15 

Internet Explorer 8

Test 1: this.a(); 
    168.2 
Test 2: a.call(this); 
    197.94 
Test 4: this.z(); 
    169.6 
Test 5: z.call(this); 
    199.02 

Firefox e Internet Explorer ha prodotto risultati su come mi aspettavo. Test 1 e Test 4 sono relativamente vicini, Test 2 e Test 5 sono relativamente vicini, e Test 2 e Test 5 impiegano più tempo del Test 1 e del Test 4 perché esiste una chiamata di funzione extra da elaborare.

Chrome Non capisco affatto, ma è molto più veloce, forse il tweaking delle prestazioni inferiori al millisecondo non è necessario.

Qualcuno ha una buona spiegazione dei risultati? Qual è il modo migliore per chiamare più volte i metodi JavaScript?

+2

è possibile aggiungere questo link nella tua domanda, in modo che altri utenti possano eseguire i test stessi? - http://jsfiddle.net/Lbbx5/ – Anurag

+1

Articoli correlati: [Widget JavaScript senza "this"] (http://michaux.ca/articles/javascript-widgets-without-this) –

+1

FYI, utilizzando Chromium (Linux) sul mio sistema (relativamente lento), dopo un paio di test sembra che i risultati siano gli stessi di Firefox (solo più veloci): le combinazioni 1/4 e 2/5 sono più o meno vicine e 2 e 5 sono più lente di 1 e 4. –

risposta

1

Bene, fino a quando il tuo sito web ha utenti IE8 come visitatori, questo è abbastanza irrilevante. Usa 1 o 3 (gli utenti non vedranno una differenza).

Probabilmente non c'è una buona risposta alla domanda "perché". Quando si tratta di ottimizzazione, è probabile che questi motori di script si concentrino sull'ottimizzazione degli scenari che vedono accadere molto nella vita reale, in cui è possibile dimostrare che l'ottimizzazione funziona correttamente e dove fa la differenza e in un modo che invalida la minima quantità di test.

+0

In qualsiasi applicazione reale che deve iterare milioni di volte, probabilmente chiamerà funzioni più complicate di value + = 'a' e probabilmente chiamerà più di una funzione per iterazione. In questo test l'ottimizzazione di circa trenta millisecondi su Internet Explorer può essere irrilevante, ma quando l'applicazione impiega più tempo a fare effettivamente qualcosa e che i trenta millisecondi vengono moltiplicati per una dozzina di chiamate di funzione, inizia a generare un ritardo molto evidente. – Killthesand

2

Proprio teorizzare, in modo da prendere questo con un grano di sale ...

motore Javascript di Chrome, V8, utilizza una tecnica di ottimizzazione chiamata Classi nascosti. Fondamentalmente costruisce oggetti statici che ombreggiano oggetti JavaScript dinamici, in cui ogni proprietà/metodo è mappato su un indirizzo di memoria fisso che può essere immeditaly referenziato senza la necessità di una costosa operazione di ricerca di tabelle. Ogni volta che un oggetto Javascript ha una proprietà aggiunta/rimossa, viene creata una nuova classe nascosta.

La mia teoria per i risultati del test con Chrome, è che il riferimento alla funzione in una variabile locale libera interrompe la relazione di classe nascosta. Anche se il riferimento alle variabili locali probabilmente non richiede una ricerca nella tabella, ora deve essere eseguito un ulteriore passaggio per riassegnare la variabile "this". Per un metodo su una classe nascosta, "questo" è un valore fisso, quindi può essere invocato senza questo passaggio.

Ancora una volta solo teorizzazione. Potrebbe valere la pena di effettuare un test per confrontare la differenza tra riferimenti alle variabili locali e riferimenti object.member in Chrome, per vedere se l'impatto sulle prestazioni per quest'ultimo è significativamente minore rispetto ad altri browser, presumibilmente a causa delle Classi nascoste.

+0

Il concetto di classi nascoste nel motore V8 lo rende ancora più strano. Perché chiamare this.a() è molto più costoso che chiamare this.z()? Chiamare a.call (questo) è meno costoso di chiamare this.a() ma chiamare z.call (this) è più costoso che chiamare this.z(). Stanno invertendo l'iterazione attraverso i membri invece dei puntatori alla memoria di dereferenziazione? Mi chiedo se questi risultati siano affidabili.Siamo sulla scala di 3-5 millisecondi; Probabilmente potrei spostare il mouse sullo schermo e distorcere questi risultati. Proverò un numero molto grande di iterazioni su Chrome e vediamo come funziona. – Killthesand

+0

Ho eseguito un altro test su Chrome, 1000 iterazioni con un milione di chiamate ciascuna. Ci è voluto un po 'di tempo per eseguire, ma ha fornito risultati molto più coerenti. Test 1: this.a(); 168.475 Test 2: a.call (questo); 172.069 Test 4: this.z(); 168.936 Test 5: z.call (questo); 173.012 – Killthesand

Problemi correlati