javascript - son - sintaxis de bucle for
Rendimiento del bucle de JavaScript: por qué disminuir el iterador hacia 0 más rápido que incrementar (11)
En su libro Even Faster Web Sites Steve Sounders escribe que una forma simple de mejorar el rendimiento de un bucle es decrementar el iterador hacia 0 en lugar de incrementarlo hacia la longitud total (en realidad, el capítulo fue escrito por Nicholas C. Zakas ). Este cambio puede generar ahorros de hasta 50% del tiempo de ejecución original, según la complejidad de cada iteración. Por ejemplo:
var values = [1,2,3,4,5];
var length = values.length;
for (var i=length; i--;) {
process(values[i]);
}
Esto es casi idéntico para el bucle for
, el bucle do-while
while y el bucle while.
Me pregunto, ¿cuál es el motivo de esto? ¿Por qué disminuir el iterador mucho más rápido? (Me interesan los antecedentes técnicos de esto y no los puntos de referencia que prueban esta afirmación).
EDITAR: A primera vista, la sintaxis de bucle utilizada aquí parece incorrecta. No hay length-1
o i>=0
, así que aclaremos (también estaba confundido).
Aquí está la sintaxis de bucle general para:
for ([initial-expression]; [condition]; [final-expression])
statement
expresión inicial -
var i=length
Esta declaración de variable se evalúa primero.
condición -
i--
Esta expresión se evalúa antes de cada iteración de bucle. Disminuirá la variable antes del primer pase a través del ciclo. Si esta expresión se evalúa como
false
el ciclo finaliza. En JavaScript es0 == false
así que si finalmente es igual a0
, se interpreta comofalse
y el ciclo finaliza.expresión final
Esta expresión se evalúa al final de cada iteración de bucle (antes de la siguiente evaluación de la condición ). No es necesario aquí y está vacío. Las tres expresiones son opcionales en un ciclo for.
La sintaxis de bucle for no es parte de la pregunta, pero debido a que es un poco poco común, creo que es interesante aclararlo. Y quizás una razón por la que es más rápido es porque usa menos expresiones (el 0 == false
"truco").
for
incrementos vs. decrementos en 2017
En los motores JS modernos, el incremento for
bucles for
es generalmente más rápido que la disminución (en base a las pruebas personales Benchmark.js), también más convencional:
for (let i = 0; i < array.length; i++) { ... }
Depende de la plataforma y la longitud de la matriz si length = array.length
tiene un efecto positivo considerable, pero por lo general no:
for (let i = 0, length = array.length; i < length; i++) { ... }
Las versiones recientes de V8 (Chrome, Node) tienen optimizaciones para array.length
, por lo que length = array.length
se puede omitir de manera eficiente en cualquier caso.
¿Lo has calculado tú mismo? El Sr. Sounders podría estar equivocado con respecto a los intérpretes modernos. Este es precisamente el tipo de optimización en la que un buen escritor de compiladores puede marcar una gran diferencia.
¿Qué pasa con el uso de un ciclo while inverso, entonces:
var values = [1,2,3,4,5];
var i = values.length;
/* i is 1st evaluated and then decremented, when i is 1 the code inside the loop
is then processed for the last time with i = 0. */
while(i--)
{
//1st time in here i is (length - 1) so it''s ok!
process(values[i]);
}
OMI, al menos este es un código más leíble que for(i=length; i--;)
Creo que la razón se debe a que está comparando el punto final del ciclo con el 0, que es más rápido que la comparación < length
(u otra variable JS).
Es porque los operadores ordinales <, <=, >, >=
son polimórficos, por lo que estos operadores requieren comprobaciones de tipo en los lados izquierdo y derecho del operador para determinar qué comportamiento de comparación se debe utilizar.
Hay algunos puntos de referencia muy buenos disponibles aquí:
¿Cuál es la forma más rápida de codificar un bucle en JavaScript?
Es fácil decir que una iteración puede tener menos instrucciones. Vamos a comparar estos dos:
for (var i=0; i<length; i++) {
}
for (var i=length; i--;) {
}
Cuando se cuenta cada acceso variable y cada operador como una instrucción, el primer ciclo for
usa 5 instrucciones (leer i
, leer length
, evaluar i<length
, prueba (i<length) == true
, incrementar i
) mientras que el último usa solo 3 instrucciones (léase i
, prueba i == true
, decremento i
). Esa es una proporción de 5: 3.
Hay una versión aún más "performante" de esto. Como cada argumento es opcional en los bucles, puede omitir incluso el primero.
var array = [...];
var i = array.length;
for(;i--;) {
do_teh_magic();
}
Con esto omites incluso el control de [initial-expression]
. Entonces terminas con solo una operación más.
No estoy seguro acerca de Javascript, y bajo compiladores modernos probablemente no importe, pero en los "viejos tiempos" este código:
for (i = 0; i < n; i++){
.. body..
}
generaría
move register, 0
L1:
compare register, n
jump-if-greater-or-equal L2
-- body ..
increment register
jump L1
L2:
mientras que el código de recuento hacia atrás
for (i = n; --i>=0;){
.. body ..
}
generaría
move register, n
L1:
decrement-and-jump-if-negative register, L2
.. body ..
jump L1
L2:
así que dentro del ciclo solo está haciendo dos instrucciones adicionales en lugar de cuatro.
No estoy seguro de si es más rápido, pero una razón que veo es que cuando iteras sobre una matriz de elementos grandes usando incrementos, tiendes a escribir:
for(var i = 0; i < array.length; i++) {
...
}
En esencia, está accediendo a la propiedad de longitud de la matriz N (número de elementos) veces. Mientras que cuando disminuyes, accedes solo una vez. Esa podría ser una razón.
Pero también puedes escribir el ciclo de incremento de la siguiente manera:
for(var i = 0, len = array.length; i < len; i++) {
...
}
Realicé un punto de referencia en C # y C ++ (sintaxis similar). Allí, en realidad, el rendimiento difiere esencialmente en los bucles for
, en comparación con do while
o while
. En C ++, el rendimiento es mayor al aumentar. También puede depender del compilador.
En Javascript, creo que todo depende del navegador (motor de Javascript), pero este comportamiento es de esperar. Javascript está optimizado para trabajar con DOM. Imagine que recorre una colección de elementos DOM que obtiene en cada iteración, e incrementa un contador, cuando tiene que eliminarlos. Se elimina el elemento 0
, luego 1
elemento, pero luego se salta el que ocupa el lugar de 0
. Al retroceder, ese problema desaparece. Sé que el ejemplo dado no es el correcto, pero encontré situaciones en las que tuve que eliminar elementos de una colección de objetos en constante cambio.
Debido a que el bucle invertido es más inevitable que el bucle directo, supongo que el motor JS está optimizado solo para eso.
También he estado explorando la velocidad de bucle, y estaba interesado en encontrar este tidbit sobre la disminución de la velocidad de incremento. Sin embargo, aún no he encontrado una prueba que demuestre esto. Hay muchos puntos de referencia de bucle en jsperf. Aquí hay uno que prueba decrementar:
http://jsperf.com/array-length-vs-cached/6
Sin embargo, el almacenamiento en caché de la longitud de la matriz (también recomienda el libro de Steve Souders) parece ser una optimización ganadora.
en los motores JS modernos, la diferencia entre los bucles de avance y retroceso ya casi no existe. Pero la diferencia de rendimiento se reduce a 2 cosas:
a) búsqueda adicional de cada propiedad de longitud en cada ciclo
//example:
for(var i = 0; src.length > i; i++)
//vs
for(var i = 0, len = src.length; len > i; i++)
esta es la mayor ganancia de rendimiento de un ciclo inverso, y obviamente se puede aplicar a bucles hacia adelante.
b) asignación de variable adicional.
la ganancia más pequeña de un ciclo inverso es que solo requiere una asignación de variable en lugar de 2
//example:
var i = src.length; while(i--)