¿por qué la última función es un 10% más rápida aunque debe crear las variables una y otra vez?

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';
    }
  };
})();

Y la función más rápida: (tenga en cuenta que siempre debe calcular las mismas variables kb / mb / gb una y otra vez). ¿Dónde gana rendimiento?

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';
 }
};
    
pregunta Tomy 15.03.2015 - 14:04
fuente

3 respuestas

23

Todos los motores de JavaScript modernos hacen una compilación justo a tiempo. No puede hacer ninguna presunción sobre lo que "debe crear una y otra vez". Ese tipo de cálculo es relativamente fácil de optimizar, en cualquier caso.

Por otra parte, el cierre sobre variables constantes no es un caso típico para el que se centraría la compilación JIT. Normalmente, creas un cierre cuando quieres poder cambiar esas variables en diferentes invocaciones. También está creando una desreferencia de puntero adicional para acceder a esas variables, como la diferencia entre acceder a una variable miembro y un int local en OOP.

Este tipo de situación es la razón por la cual las personas desechan la línea de "optimización prematura". Las optimizaciones fáciles ya están hechas por el compilador.

    
respondido por el Karl Bielefeldt 15.03.2015 - 15:19
fuente
12

Las variables son baratas. Los contextos de ejecución y las cadenas de alcance son caros.

Hay varias respuestas que esencialmente se reducen a "porque los cierres", y esas son esencialmente verdaderas, pero el problema no es específicamente con el cierre, es el hecho de que tiene una función de referencia Variables en un ámbito diferente. Tendría el mismo problema si estas fueran variables globales en el objeto window , a diferencia de las variables locales dentro del IIFE. Pruébalo y mira.

Entonces, en su primera función, cuando el motor ve esta declaración:

var gbSize = size / GB;

Tiene que seguir los siguientes pasos:

  1. Busque una variable size en el alcance actual. (Lo encontré.)
  2. Busque una variable GB en el alcance actual. (No encontrado).
  3. Busque una variable GB en el ámbito principal. (Lo encontré.)
  4. Haz el cálculo y asigna a gbSize .

El paso 3 es considerablemente más costoso que simplemente asignar una variable. Además, haz esto cinco veces , incluyendo dos veces para GB y MB . Sospecho que si alias estos al principio de la función (por ejemplo, var gb = GB ) y hiciste referencia al alias en su lugar, en realidad produciría una pequeña aceleración, aunque también es posible que algunos motores JS ya realicen esta optimización. Y, por supuesto, la forma más efectiva de acelerar la ejecución es simplemente no atravesar la cadena de alcance en absoluto.

Tenga en cuenta que JavaScript no es como un lenguaje compilado, de tipo estático, en el que el compilador resuelve estas direcciones de variables en el momento de la compilación. El motor JS tiene que resolverlos por nombre , y estas búsquedas se realizan en tiempo de ejecución, cada vez. Así que quieres evitarlos cuando sea posible.

La asignación de variables es extremadamente barata en JavaScript. En realidad podría ser la operación más barata, aunque no tengo nada que respalde esa afirmación. No obstante, es seguro decir que es casi nunca una buena idea intentar evitar crear variables; Casi cualquier optimización que intentes hacer en esa área en realidad va a empeorar las cosas, en lo que respecta al rendimiento.

    
respondido por el Aaronaught 15.03.2015 - 19:49
fuente
2

Un ejemplo implica un cierre, el otro no. Implementar cierres es un poco complicado, ya que las variables cerradas no funcionan como las variables normales. Esto es más obvio en un lenguaje de bajo nivel como C, pero usaré JavaScript para ilustrar esto.

Un cierre no solo consiste en una función, sino también en todas las variables sobre las que se cerró. Cuando queremos invocar esa función, también debemos proporcionar todas las variables cerradas. Podemos modelar un cierre por una función que recibe un objeto como primer argumento que representa estas variables cerradas sobre:

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

Tenga en cuenta que la convención de llamadas extrañas closure.apply(closure, ...realArgs) esto requiere

El soporte de objetos integrado de JavaScript hace posible omitir el argumento explícito vars , y nos permite usar this en su lugar:

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

Esos ejemplos son equivalentes a este código que realmente usa cierres:

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

En este último ejemplo, el objeto solo se usa para agrupar las dos funciones devueltas; El enlace this es irrelevante. Todos los detalles para hacer posibles los cierres (pasar datos ocultos a la función real, cambiar todos los accesos a las variables de cierre y buscar en esos datos ocultos) están a cargo del idioma.

Pero los cierres de llamadas implican la sobrecarga de pasar esos datos adicionales, y la ejecución de un cierre implica la sobrecarga de las búsquedas en esos datos adicionales, empeorada por la mala ubicación de la memoria caché y, por lo general, una falta de referencia de puntero cuando se compara con las variables comunes, por lo que es No sorprende que una solución que no se basa en cierres funcione mejor. Especialmente, ya que todo lo que le permite hacer el cierre es un par de operaciones aritméticas extremadamente baratas, que incluso se pueden plegar constantemente durante el análisis.

    
respondido por el amon 15.03.2015 - 16:35
fuente

Lea otras preguntas en las etiquetas