2010-08-28 13 views
6

He estado investigando el rendimiento de JavaScript. Aprendí que al acceder más de una vez, generalmente es mejor copiar las variables de cierre y los miembros de la clase en el alcance local para acelerar las cosas. Por ejemplo:Al ajustar el rendimiento, ¿cuál es la mejor manera de llamar a los métodos de JavaScript varias veces?

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

Entiendo esto; Agrega un atajo para que el intérprete busque el valor por su nombre. Este concepto se vuelve muy poco claro cuando se trata de métodos. Al principio, pensé que funcionaría de la misma manera. Por ejemplo:

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

Como está, esto no funcionará en absoluto. Acceder al método de esta manera lo quita de su alcance. Cuando alcanza la línea this.value, esto se refiere al objeto window y this.value probablemente no estará definido. En lugar de llamar directamente a objFn y perder alcance, podría volver a pasar su alcance dentro de él con objFn.call (obj), pero ¿funciona mejor o peor que el obj.fn original()?

Decidí escribir un script para probar esto y obtuve resultados muy confusos. Este script realiza iteraciones sobre varias pruebas que repiten varias veces las llamadas a la función anterior. El tiempo promedio tomado para cada prueba se envía al cuerpo.

Un objeto se crea con muchos métodos simples en él. Los métodos adicionales están ahí para determinar si el intérprete tiene que trabajar mucho más para encontrar un método específico.

La Prueba 1 simplemente llama this.a();
La prueba 2 crea una variable local a = this.a luego llama a.call (this);
La prueba 3 crea una variable local usando la función de vinculación de YUI para preservar el alcance. Comenté esto. Las llamadas a funciones adicionales creadas por YUI hacen que esto sea más lento.

Las pruebas 4, 5 y 6 son copias de 1, 2, 3, excepto que se usa z en lugar de a.

La función posterior de YUI se usa para evitar errores de script fuera de control. La sincronización se realiza en los métodos de prueba reales, por lo que setTimeouts no debe afectar los resultados. Cada función se llama un total de 10000000 veces. (Fácilmente configurable si desea ejecutar pruebas.)

Aquí está mi documento completo XHTML que solía probar.

<!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> 

He ejecutado esto con Windows 7 en tres navegadores diferentes. Estos resultados están en milisegundos.

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 produjo resultados acerca de la forma en que esperaba. La Prueba 1 y la Prueba 4 están relativamente cerca, la Prueba 2 y la Prueba 5 están relativamente cerca, y la Prueba 2 y la Prueba 5 tardan más tiempo que la Prueba 1 y la Prueba 4 porque hay una llamada de función adicional para procesar.

Chrome No lo entiendo para nada, pero es mucho más rápido, quizás no sea necesario ajustar el rendimiento en menos de milisegundos.

¿Alguien tiene una buena explicación de los resultados? ¿Cuál es la mejor manera de llamar a los métodos de JavaScript varias veces?

+2

se puede añadir este enlace en su pregunta, por lo que otros usuarios pueden ejecutar las mismas pruebas? - http://jsfiddle.net/Lbbx5/ – Anurag

+1

Related read: [JavaScript Widgets sin "this"] (http://michaux.ca/articles/javascript-widgets-without-this) –

+1

FYI, usando Chromium (Linux) en mi sistema (relativamente lento), después de un par de pruebas parece que los resultados son casi los mismos que en Firefox (solo que más rápido): las combinaciones 1/4 y 2/5 son más o menos cercanas y 2 y 5 son más lentas que 1 y 4. –

Respuesta

1

Bueno, siempre y cuando su sitio web tenga como visitantes a los usuarios de IE8, esto es completamente irrelevante. Use 1 o 3 (los usuarios no verán la diferencia).

Probablemente no haya una buena respuesta a la pregunta "por qué". Cuando se trata de optimización, es probable que estos motores de script se centren en optimizar escenarios que ven que suceden mucho en la vida real, donde se puede demostrar que la optimización funciona correctamente, y donde hace la diferencia, y de una manera que invalida la menor cantidad de pruebas

+0

En cualquier aplicación real que tenga que iterar millones de veces, probablemente invoque funciones más complicadas que value + = 'a' y probablemente invoque más de una función por iteración. En esta prueba, la optimización de aproximadamente treinta milisegundos en Internet Explorer puede ser irrelevante, pero cuando la aplicación tarda más tiempo en hacer algo y se multiplican treinta milisegundos por un par de docenas de llamadas a funciones, comienza a sumar un retraso muy notable. – Killthesand

2

Sólo teorizar, por lo que tomar esto con un grano de sal ...

motor de Javascript de Chrome, V8, utiliza una técnica de optimización llamada Clases ocultos. Básicamente, construye objetos estáticos que sombrean objetos Javascript dinámicos, donde cada propiedad/método se asigna a una dirección de memoria fija a la que se puede hacer referencia inmediatamente sin necesidad de una costosa operación de búsqueda de tablas. Cada vez que un objeto Javascript tiene una propiedad añadida/eliminada, se crea una nueva clase oculta.

Mi teoría para los resultados de su prueba con Chrome, es que al hacer referencia a la función en una variable local libre se rompe la relación de clase oculta. Si bien las referencias a las variables locales probablemente tampoco requieran una búsqueda de tabla, ahora se debe realizar un paso adicional al volver a asignar la variable 'this'. Para un método en una clase oculta, 'this' es un valor fijo, por lo que puede invocarse sin este paso.

Nuevamente teorizando. Puede valer la pena realizar una prueba para comparar la diferencia entre las referencias de variables locales y las referencias a object.member en Chrome, para ver si el acierto para el rendimiento de este último es significativamente menor que en otros navegadores, presumiblemente debido a Clases ocultas.

+0

El concepto de clases ocultas en el motor V8 lo hace aún más extraño. ¿Por qué llamar this.a() es mucho más caro que llamar this.z()? Llamar a .call (esto) es menos costoso que llamar this.a() pero llamar a z.call (esto) es más costoso que llamar a this.z(). ¿Se están iterando de forma inversa a través de los miembros en lugar de eliminar la referencia de los punteros de memoria? Me pregunto si estos resultados son confiables.Estamos en la escala de 3-5 milisegundos; Probablemente podría mover el mouse por la pantalla y distorsionar estos resultados. Intentaré una gran cantidad de iteraciones en Chrome y veré cómo funciona. – Killthesand

+0

He realizado otra prueba en Chrome, 1000 iteraciones con un millón de llamadas cada una. Tardó bastante tiempo en ejecutarse, pero proporcionó resultados mucho más consistentes. Prueba 1: this.a(); 168.475 Prueba 2: a.call (esto); 172.069 Prueba 4: this.z(); 168.936 Prueba 5: z.call (esto); 173.012 – Killthesand

Cuestiones relacionadas