v8 ¿Implicaciones de rendimiento de JavaScript de const, let y var?

88

Independientemente de las diferencias funcionales, ¿el uso de las nuevas palabras clave 'let' y 'const' tiene algún impacto generalizado o específico en el rendimiento en relación con 'var'?

Después de ejecutar el programa:

function timeit(f, N, S) {
    var start, timeTaken;
    var stats = {min: 1e50, max: 0, N: 0, sum: 0, sqsum: 0};
    var i;
    for (i = 0; i < S; ++i) {
        start = Date.now();
        f(N);
        timeTaken = Date.now() - start;

        stats.min = Math.min(timeTaken, stats.min);
        stats.max = Math.max(timeTaken, stats.max);
        stats.sum += timeTaken;
        stats.sqsum += timeTaken * timeTaken;
        stats.N++
    }

    var mean = stats.sum / stats.N;
    var sqmean = stats.sqsum / stats.N;

    return {min: stats.min, max: stats.max, mean: mean, spread: Math.sqrt(sqmean - mean * mean)};
}

var variable1 = 10;
var variable2 = 10;
var variable3 = 10;
var variable4 = 10;
var variable5 = 10;
var variable6 = 10;
var variable7 = 10;
var variable8 = 10;
var variable9 = 10;
var variable10 = 10;

function varAccess(N) {
    var i, sum;
    for (i = 0; i < N; ++i) {
        sum += variable1;
        sum += variable2;
        sum += variable3;
        sum += variable4;
        sum += variable5;
        sum += variable6;
        sum += variable7;
        sum += variable8;
        sum += variable9;
        sum += variable10;
    }
    return sum;
}

const constant1 = 10;
const constant2 = 10;
const constant3 = 10;
const constant4 = 10;
const constant5 = 10;
const constant6 = 10;
const constant7 = 10;
const constant8 = 10;
const constant9 = 10;
const constant10 = 10;

function constAccess(N) {
    var i, sum;
    for (i = 0; i < N; ++i) {
        sum += constant1;
        sum += constant2;
        sum += constant3;
        sum += constant4;
        sum += constant5;
        sum += constant6;
        sum += constant7;
        sum += constant8;
        sum += constant9;
        sum += constant10;
    }
    return sum;
}


function control(N) {
    var i, sum;
    for (i = 0; i < N; ++i) {
        sum += 10;
        sum += 10;
        sum += 10;
        sum += 10;
        sum += 10;
        sum += 10;
        sum += 10;
        sum += 10;
        sum += 10;
        sum += 10;
    }
    return sum;
}

console.log("ctl = " + JSON.stringify(timeit(control, 10000000, 50)));
console.log("con = " + JSON.stringify(timeit(constAccess, 10000000, 50)));
console.log("var = " + JSON.stringify(timeit(varAccess, 10000000, 50)));

.. Mis resultados fueron los siguientes:

ctl = {"min":101,"max":117,"mean":108.34,"spread":4.145407097016924}
con = {"min":107,"max":572,"mean":435.7,"spread":169.4998820058587}
var = {"min":103,"max":608,"mean":439.82,"spread":176.44417700791374}

Sin embargo, la discusión como se indica aquí parece indicar un potencial real de diferencias de rendimiento en ciertos escenarios: https://esdiscuss.org/topic/performance-concern-with-let-const

sean2078
fuente
Creo que eso depende del uso, por ejemplo, letusado en el alcance del bloque debería ser más eficiente que var, que no tiene alcance del bloque, sino solo alcance de la función.
adeneo
Si puedo preguntar, ¿por qué es @adeneo?
sean2078
1
@ sean2078: si necesita declarar una variable que solo vive en un alcance de bloque, letlo haría y luego se recolectaría la basura, mientras varque, que tiene un alcance de función, no necesariamente funcionaría de la misma manera. Nuevamente, creo que es tan específico para el uso, que ambos lety const pueden ser más eficaces, pero no siempre lo serán.
adeneo
1
Estoy confundido por cómo el código citado pretende demostrar alguna diferencia entre vary let: Nunca se usa leten absoluto.
TJ Crowder
1
Actualmente no lo hace, solo const vs.var .. Originalmente obtenido de gist.github.com/srikumarks/1431640 (crédito a srikumarks), sin embargo, se solicitó que se cuestionara el código
sean2078

Respuestas:

116

TL; DR

En teoría , una versión no optimizada de este ciclo:

for (let i = 0; i < 500; ++i) {
    doSomethingWith(i);
}

podría ser más lento que una versión no optimizada del mismo bucle con var:

for (var i = 0; i < 500; ++i) {
    doSomethingWith(i);
}

porque se crea una variable diferente i para cada iteración del ciclo con let, mientras que solo hay una icon var.

Un argumento en contra es el hecho de que varestá elevado, por lo que se declara fuera del ciclo, mientras letque solo se declara dentro del ciclo, lo que puede ofrecer una ventaja de optimización.

En la práctica , aquí en 2018, los motores JavaScript modernos hacen suficiente introspección del bucle para saber cuándo puede optimizar esa diferencia. (Incluso antes de eso, lo más probable es que su bucle estuviera funcionando lo suficiente como para que la letsobrecarga adicional relacionada se elimine de todos modos. Pero ahora ni siquiera tiene que preocuparse por eso).

Tenga cuidado con los puntos de referencia sintéticos, ya que es muy fácil equivocarse y activan los optimizadores de motor de JavaScript de formas que el código real no hace (tanto las buenas como las malas). Sin embargo, si desea un punto de referencia sintético, aquí tiene uno:

Dice que no hay una diferencia significativa en esa prueba sintética en V8 / Chrome o SpiderMonkey / Firefox. (Las pruebas repetidas en ambos navegadores hacen que uno gane, o el otro gane, y en ambos casos dentro de un margen de error). Pero nuevamente, es un punto de referencia sintético, no su código. Preocúpese por el rendimiento de su código cuando y si su código tiene un problema de rendimiento.

Por una cuestión de estilo, prefiero letpara el beneficio de alcance y el beneficio de cierre en bucles si uso la variable de bucle en un cierre.

Detalles

La diferencia importante entre vary leten un forbucle es que ise crea una diferente para cada iteración; aborda el clásico problema de "cierres en bucle":

Crear el nuevo EnvironmentRecord para cada cuerpo de bucle ( enlace de especificación ) es un trabajo y el trabajo lleva tiempo, por lo que, en teoría, la letversión es más lenta que la varversión.

Pero la diferencia solo importa si crea una función (cierre) dentro del bucle que usa i, como hice en el ejemplo de fragmento ejecutable anterior. De lo contrario, la distinción no se puede observar y se puede optimizar.

Aquí, en 2018, parece que V8 (y SpiderMonkey en Firefox) están haciendo suficiente introspección para que no haya costos de rendimiento en un bucle que no utiliza la letsemántica de variable por iteración. Vea esta prueba .


En algunos casos, constpuede proporcionar una oportunidad de optimización que varno lo haría, especialmente para las variables globales.

El problema con una variable global es que es, bueno, global; cualquier código en cualquier lugar podría acceder a él. Entonces, si declara una variable con la varque nunca tiene la intención de cambiar (y nunca cambia su código), el motor no puede asumir que nunca cambiará como resultado de un código cargado más tarde o similar.

Con const, sin embargo, le está diciendo explícitamente al motor que el valor no puede cambiar¹. Por lo tanto, es libre de hacer cualquier optimización que desee, incluida la emisión de una referencia literal en lugar de una variable al código que la usa, sabiendo que los valores no se pueden cambiar.

¹ Recuerde que con los objetos, el valor es una referencia al objeto, no al objeto en sí. Entonces const o = {}, con , puede cambiar el estado del objeto ( o.answer = 42), pero no puede oseñalar un nuevo objeto (porque eso requeriría cambiar la referencia del objeto que contiene).


Cuando se usa leto consten otras varsituaciones similares, no es probable que tengan un rendimiento diferente. Esta función debería tener exactamente el mismo rendimiento si usa varo let, por ejemplo:

function foo() {
    var i = 0;
    while (Math.random() < 0.5) {
        ++i;
    }
    return i;
}

Por supuesto, todo es poco probable que importe y es algo de lo que preocuparse solo si hay un problema real que resolver.

TJ Crowder
fuente
Gracias por la respuesta, estoy de acuerdo, por lo que yo mismo he estandarizado el uso de var para operaciones de bucle como se indica en su primer ejemplo de bucle for, y deje / const para todas las demás declaraciones asumiendo que la diferencia de rendimiento es esencialmente inexistente como parecería la prueba de rendimiento para indicar por ahora. Quizás más adelante, se agregarán optimizaciones en const. Es decir, a menos que alguien más pueda mostrar una diferencia perceptible a través del ejemplo de código.
sean2078
@ sean2078: también lo uso leten el ejemplo de bucle. No vale la pena preocuparse por la diferencia de rendimiento en el caso del 99,999%.
TJ Crowder
2
A mediados de 2018, las versiones con let y var tienen la misma velocidad en Chrome, por lo que ahora ya no hay diferencia.
Máximo
1
@DanM .: Buenas noticias, la optimización parece haberse puesto al día, al menos en V8 y SpiderMonkey. :-)
TJ Crowder
1
Gracias. Lo suficientemente justo.
hypers
18

"LET" ES MEJOR EN LAS DECLARACIONES DE BUCLE

Con una simple prueba (5 veces) en un navegador así:

// WITH VAR
console.time("var-time")
for(var i = 0; i < 500000; i++){}
console.timeEnd("var-time")

El tiempo medio de ejecución es de más de 2,5 ms.

// WITH LET
console.time("let-time")
for(let i = 0; i < 500000; i++){}
console.timeEnd("let-time")

El tiempo medio de ejecución es de más de 1,5 ms.

Descubrí que el tiempo de ciclo con let es mejor.

Amn
fuente
6
Al ejecutar esto en Firefox 65.0, obtuve velocidades medias de var=138.8msy let=4ms. Eso no es un error tipográfico, letes más de 30 veces más rápido en este momento
Katamari
6
Acabo de probar esto en Node v12.5. Encontré que las velocidades medias son var=2.6msy let=1.0ms. Así que dejar entrar a Node es un poco más del doble de rápido.
Kane Hooper
2
Solo para señalar el punto habitual de que las pruebas de rendimiento son difíciles en presencia de optimizadores: creo que el bucle let se está optimizando por completo; deje que solo exista dentro del bloque y el bucle no tiene efectos secundarios y V8 es lo suficientemente inteligente como para saber que puede simplemente quite el bloque, luego el bucle. la declaración var está levantada por lo que no puede saber eso. Sus bucles, tal como están, obtengo 1ms / 0.4ms, sin embargo, si para ambos tengo una variable j (var o let) fuera del bucle que también se incrementa, obtengo 1ms / 1.5ms. es decir, el ciclo var no cambia, ahora el ciclo tarda más.
Euan Smith
@KaneHooper: si obtuvo una diferencia de cinco veces en Firefox, tendrá que haber sido el cuerpo del bucle vacío el que lo hizo. Los bucles reales no tienen cuerpos vacíos.
TJ Crowder
Tenga cuidado con los benchmarks sintéticos y, en particular, con los bucles con cuerpos vacíos. Si realmente hace algo en el ciclo, este punto de referencia sintético (que, nuevamente, ¡tenga cuidado! :-)) sugiere que no hay una diferencia significativa. También agregué uno a mi respuesta para que esté en el sitio (no como esas pruebas jsPerf que seguían desapareciendo en mí. :-)). Las carreras repetidas muestran que uno gana o el otro gana. Ciertamente, nada concluyente.
TJ Crowder
8

La respuesta de TJ Crowder es excelente.

Aquí hay una adición de: "¿Cuándo sacaré el máximo provecho de mi inversión en la edición de declaraciones var existentes a const?"

Descubrí que la mayor mejora del rendimiento tenía que ver con las funciones "exportadas".

Entonces, si los archivos A, B, R y Z están llamando a una función de "utilidad" en el archivo U que se usa comúnmente a través de su aplicación, entonces cambiar esa función de utilidad a "const" y la referencia del archivo principal a una const puede eak un rendimiento mejorado. Me pareció que no era mucho más rápido, pero el consumo total de memoria se redujo en aproximadamente un 1-3% para mi aplicación Frankenstein-ed extremadamente monolítica. Lo cual, si está gastando bolsas de dinero en efectivo en la nube o en su servidor baremetal, podría ser una buena razón para dedicar 30 minutos a revisar y actualizar algunas de esas declaraciones var a const.

Me doy cuenta de que si lees cómo const, var y dejas trabajar bajo las sábanas, probablemente ya hayas concluido lo anterior ... pero en caso de que "echas un vistazo": D.

Por lo que recuerdo de la evaluación comparativa en el nodo v8.12.0 cuando estaba haciendo la actualización, mi aplicación pasó de un consumo inactivo de ~ 240 MB de RAM a ~ 233 MB de RAM.

isaacdre
fuente
2

La respuesta de TJ Crowder es muy buena pero:

  1. 'let' está hecho para hacer que el código sea más legible, no más poderoso
  2. por teoría vamos a ser más lento que var
  3. con la práctica, el compilador no puede resolver completamente (análisis estático) un programa incompleto, por lo que en algún momento perderá la optimización
  4. en cualquier caso, el uso de 'let' requerirá más CPU para la introspección, el banco debe iniciarse cuando Google v8 comience a analizar
  5. si la introspección falla, 'let' presionará con fuerza el recolector de basura V8, requerirá más iteraciones para liberar / reutilizar. también consumirá más RAM. el banco debe tener en cuenta estos puntos
  6. Google Closure transformará let in var ...

El efecto de la brecha de rendimiento entre var y let se puede ver en un programa completo de la vida real y no en un solo bucle básico.

De todos modos, usar let donde no es necesario, hace que su código sea menos legible.

Michael Valve
fuente