¿Por qué esta ú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';
 }
};
Para mi
fuente
3
En cualquier lenguaje de tipo estático, las "variables" se compilarían como constantes. Tal vez los motores JS modernos sean capaces de hacer la misma optimización. Esto parece no funcionar si las variables son parte de un cierre.
usr
66
Este es un detalle de implementación del motor de JavaScript que está utilizando. El tiempo y el espacio teóricos son los mismos, es solo la implementación de un determinado motor de JavaScript lo que los variará. Entonces, para responder a su pregunta correctamente, debe enumerar el motor JavaScript específico con el que midió estos. Quizás alguien conozca los detalles de su implementación para decir cómo / por qué hizo que uno fuera más óptimo que el otro. También debe publicar su código de medición.
Jimmy Hoffa
usa la palabra "calcular" en referencia a valores constantes; realmente no hay nada que calcular allí en lo que estás haciendo referencia. La aritmética de valores constantes es una de las optimizaciones más simples y obvias que hacen los compiladores, por lo que cada vez que vea una expresión que solo tiene valores constantes, puede suponer que toda la expresión está optimizada para un solo valor constante.
Jimmy Hoffa
@JimmyHoffa es cierto, pero por otro lado necesita crear 3 variables constantes cada llamada a función ...
Tomy
Las constantes de @Tomy no son variables. No varían, por lo que no necesitan ser recreados después de la compilación. Una constante generalmente se coloca en la memoria, y cada alcance futuro para esa constante se dirige exactamente al mismo lugar, no hay necesidad de recrearla porque su valor nunca variará , por lo tanto, no es una variable. Los compiladores generalmente no emitirán código que cree constantes, el compilador hace la creación y dirige todas las referencias de código a lo que hizo.
Jimmy Hoffa

Respuestas:

23

Todos los motores JavaScript modernos compilan justo a tiempo. No se pueden hacer presunciones sobre lo que "debe crear una y otra vez". Ese tipo de cálculo es relativamente fácil de optimizar, en cualquier caso.

Por otro lado, cerrar sobre variables constantes no es un caso típico para el que apuntaría la compilación JIT. Por lo general, crea un cierre cuando desea 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 la gente tira la línea de "optimización prematura". Las optimizaciones fáciles ya están hechas por el compilador.

Karl Bielefeldt
fuente
Sospecho que es ese recorrido de alcance para resolución variable lo que está causando la pérdida como mencionas. Parece razonable, pero quién sabe realmente qué locura se encuentra en un motor JIT de JavaScript ...
Jimmy Hoffa
1
Posible expansión de esta respuesta: la razón por la que un JIT ignoraría una optimización que es fácil para un compilador fuera de línea es porque el rendimiento de todo el compilador es más importante que en casos inusuales.
Leushenko
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 cierres", y esas son esencialmente ciertas, pero el problema no es específicamente con el cierre, es el hecho de que tiene una función que hace referencia a variables en un ámbito diferente. Tendría el mismo problema si estas fueran variables globales en el windowobjeto, a diferencia de las variables locales dentro del IIFE. Pruébalo y verás.

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

var gbSize = size / GB;

Tiene que seguir los siguientes pasos:

  1. Busca una variable sizeen el alcance actual. (Lo encontré.)
  2. Busca una variable GBen el alcance actual. (Extraviado.)
  3. Busque una variable GBen el ámbito primario. (Lo encontré.)
  4. Haga el cálculo y asigne a gbSize.

El paso 3 es considerablemente más costoso que simplemente asignar una variable. Además, haces esto cinco veces , incluyendo dos veces para ambos GBy MB. Sospecho que si los alias al principio de la función (por ejemplo var gb = GB) y haces referencia al alias, 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.

Tenga en cuenta que JavaScript no es como un lenguaje compilado, de tipo estático, donde el compilador resuelve estas direcciones variables en tiempo de compilación. El motor JS tiene que resolverlos por nombre , y estas búsquedas ocurren en tiempo de ejecución, siempre. Por lo tanto, debe 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. Sin embargo, es seguro decir que casi nunca es una buena idea tratar de evitar crear variables; casi cualquier optimización que intente hacer en esa área terminará empeorando las cosas, en términos de rendimiento.

Aaronaught
fuente
E incluso si la "optimización" no afecta negativamente al rendimiento, es casi seguro que se va a afectar negativamente a la legibilidad del código. Lo cual, a menos que esté haciendo algunas cosas informáticas locas, a menudo es una mala compensación (aparentemente no hay anclaje permanente, busque "2009-02-17 11:41"). Como dice el resumen: "Elija claridad sobre velocidad, si la velocidad no es absolutamente necesaria".
un CVn
Incluso cuando se escribe un intérprete muy básico para lenguajes dinámicos, el acceso variable durante el tiempo de ejecución tiende a ser una operación O (1), y el recorrido del alcance O (n) ni siquiera es necesario durante la compilación inicial. En cada ámbito, a cada variable recién declarada se le asigna un número, por var a, b, clo tanto, dado que podemos acceder bcomo scope[1]. Todos los ámbitos están numerados, y si este ámbito está anidado a cinco ámbitos de profundidad, entonces bse aborda por completo lo env[5][1]que se conoce durante el análisis. En el código nativo, los ámbitos corresponden a segmentos de pila. Los cierres son más complicados ya que deben respaldar y reemplazar elenv
amon
@amon: Eso podría ser cómo le gustaría idealmente como para el trabajo, pero no es cómo funciona realmente. Gente mucho más entendida y experimentada que yo he escrito libros sobre esto; en particular, te señalaría JavaScript de alto rendimiento de Nicholas C. Zakas. Aquí hay un fragmento , y también habló con puntos de referencia para respaldarlo. Por supuesto, él no es el único, solo el más conocido. JavaScript tiene un alcance léxico, por lo que los cierres en realidad no son tan especiales, esencialmente, todo es un cierre.
Aaronaught
@Aaronaught Interesante. Dado que ese libro tiene 5 años, me interesó cómo un motor JS actual maneja las búsquedas variables y miré el backend x64 del motor V8. Durante el análisis estático, la mayoría de las variables se resuelven estáticamente y se les asigna un desplazamiento de memoria en su alcance. Los ámbitos de funciones se representan como listas vinculadas, y el ensamblaje se emite como un bucle desenrollado para alcanzar el ámbito correcto. Aquí, obtendríamos el equivalente al código C *(scope->outer + variable_offset)para un acceso; cada nivel de alcance de función adicional cuesta una desreferencia de puntero adicional. Parece que ambos teníamos razón :)
amon
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 que 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:

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 la convención de llamadas incómodas que closure.apply(closure, ...realArgs)esto requiere

El soporte de objetos incorporados de JavaScript hace posible omitir el varsargumento explícito , y nos permite usar thisen 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 en realidad 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; La thisunión es irrelevante. El lenguaje se ocupa de todos los detalles de hacer posibles los cierres: pasar datos ocultos a la función real, cambiar todos los accesos a las variables de cierre a búsquedas en esos datos ocultos.

Pero llamar a los cierres implica la sobrecarga de pasar esos datos adicionales, y ejecutar un cierre implica la sobrecarga de las búsquedas en esos datos adicionales, empeorado por la mala ubicación de la caché y generalmente una desferencia de puntero en comparación con las variables ordinarias, por lo que no es sorprendente que Una solución que no se basa en cierres funciona mejor. Especialmente porque todo lo que su cierre le ahorra hacer son algunas operaciones aritméticas extremadamente baratas, que incluso podrían doblarse constantemente durante el análisis.

amon
fuente