¿Los bucles son realmente más rápidos en reversa?


268

He escuchado esto varias veces. ¿Los bucles de JavaScript son realmente más rápidos al contar hacia atrás? Si es así, ¿por qué? ¡He visto algunos ejemplos de conjuntos de pruebas que muestran que los bucles invertidos son más rápidos, pero no puedo encontrar ninguna explicación de por qué!

Supongo que es porque el ciclo ya no tiene que evaluar una propiedad cada vez que verifica si está terminado y solo verifica el valor numérico final.

Es decir

for (var i = count - 1; i >= 0; i--)
{
  // count is only evaluated once and then the comparison is always on 0.
}

66
jeje. eso tomará indefinidamente. prueba i--
StampedeXV

55
El forbucle hacia atrás es más rápido porque la variable de control del bucle de límite superior (jeje, límite inferior) no tiene que definirse o extraerse de un objeto; Es un cero constante.
Josh Stodola

88
No hay diferencia real . Las construcciones de bucles nativos siempre serán muy rápidas . No te preocupes por su desempeño.
James Allardice

24
@Afshin: Para este tipo de preguntas, por favor, nos muestran los artículos que usted se refiere.
Carreras de ligereza en órbita

22
Hay una diferencia principalmente importante para dispositivos de muy bajo consumo y con batería. La diferencia es que con i-- se compara con 0 para el final del ciclo, mientras que con i ++ se compara con el número> 0. Creo que la diferencia de rendimiento fue algo así como 20 nanosegundos (algo así como cmp ax, 0 vs cmp ax , bx) - que es nada más que si bucle miles de veces por segundo, por qué no tiene 20 nanosegundos ganancia para cada :)
Luben

Respuestas:


903

No es que i--sea ​​más rápido que i++. En realidad, ambos son igualmente rápidos.

Lo que lleva tiempo en los bucles ascendentes es evaluar, para cada uno i, el tamaño de su matriz. En este bucle:

for(var i = array.length; i--;)

Evalúa .lengthsolo una vez, cuando declara i, mientras que para este ciclo

for(var i = 1; i <= array.length; i++)

evalúa .lengthcada vez que incrementa i, cuando verifica si i <= array.length.

En la mayoría de los casos , ni siquiera debería preocuparse por este tipo de optimización .


23
¿Vale la pena introducir una variable para array.length y usarla en la cabeza del bucle for?
ragatskynet

39
@ragatskynet: No, a menos que esté configurando un punto de referencia y quiera hacer un punto.
Jon

29
@ragatskynet Depende: ¿será más rápido evaluar .lengthun cierto número de veces o declarar y definir una nueva variable? En la mayoría de los casos, esta es una optimización prematura (e incorrecta), a menos que lengthsea ​​muy costoso evaluarlo.
alestanis

10
@ Dr.Dredel: No es la comparación, es la evaluación. 0es más rápido de evaluar que array.length. Bueno, supuestamente
Carreras de ligereza en órbita

22
Lo que vale la pena mencionar es que estamos hablando de lenguajes interpretados como Ruby, Python. Los lenguajes compilados, por ejemplo, Java, tienen optimizaciones a nivel de compilador que "suavizarán" estas diferencias hasta el punto de que no importa si .lengthse declara for loopo no.
Haris Krajina

198

Este tipo comparó muchos bucles en javascript, en muchos navegadores. También tiene un conjunto de pruebas para que pueda ejecutarlos usted mismo.

En todos los casos (a menos que me perdí uno en mi lectura) el ciclo más rápido fue:

var i = arr.length; //or 10
while(i--)
{
  //...
}

Agradable :) Pero no hay un bucle "for" probado al revés ... Pero los bucles for mencionados por peirix o searlea deberían ser más o menos lo mismo que el bucle "while" con "i--" como condición. Y ese fue el bucle más rápido probado.
SvenFinke

11
Interesante, pero me pregunto si el decremento previo sería aún más rápido. Como no tendrá que almacenar el valor intermedio de i.
tvanfosson

Si no recuerdo mal, mi profesor del curso de hardware dijo que esa prueba para 0 o no 0 es el "cálculo" más rápido posible. En while (i--) la prueba siempre es una prueba para 0. ¿Quizás por eso es la más rápida?
StampedeXV

3
@tvanfosson si pre-decrementas --ientonces tienes que usar var current = arr[i-1];dentro del bucle o será desactivado por uno ...
DaveAlger

2
Escuché que i-- puede ser más rápido que --i porque en el segundo caso el procesador necesita disminuir y luego probar contra el nuevo valor (hay una dependencia de datos entre las instrucciones), mientras que en el primer caso el procesador puede pruebe el valor existente y disminuya el valor algún tiempo después. No estoy seguro de si esto se aplica a JavaScript o solo a un código C de muy bajo nivel.
marcus

128

Intento dar una imagen amplia con esta respuesta.

Lo siguiente entre paréntesis fue mi creencia hasta que recientemente probé el problema:

[[En términos de lenguajes de bajo nivel como C / C ++ , el código se compila para que el procesador tenga un comando de salto condicional especial cuando una variable es cero (o no cero).
Además, si te importa esta optimización, puedes ir en ++ilugar de hacerlo i++, porque ++ies un comando de procesador único, mientras que i++significa j=i+1, i=j.]]

Se pueden hacer bucles realmente rápidos desenrollando:

for(i=800000;i>0;--i)
    do_it(i);

Puede ser mucho más lento que

for(i=800000;i>0;i-=8)
{
    do_it(i); do_it(i-1); do_it(i-2); ... do_it(i-7);
}

pero las razones para esto pueden ser bastante complicadas (solo por mencionar, hay problemas de preprocesamiento de comandos de procesador y manejo de caché en el juego).

En términos de lenguajes de alto nivel , como JavaScript, tal como lo solicitó, puede optimizar las cosas si confía en bibliotecas, funciones integradas para bucles. Déjelos decidir cómo se hace mejor.

En consecuencia, en JavaScript, sugeriría usar algo como

array.forEach(function(i) {
    do_it(i);
});

También es menos propenso a errores y los navegadores tienen la oportunidad de optimizar su código.

[OBSERVACIÓN: no solo los navegadores, sino que también tiene un espacio para optimizar fácilmente, simplemente redefina la forEachfunción (dependiendo del navegador) para que utilice el mejor truco más reciente. :) @AMK dice que en casos especiales vale la pena usar array.popo array.shift. Si haces eso, ponlo detrás de la cortina. La mayor exageración es agregar opciones forEachpara seleccionar el algoritmo de bucle.]

Además, también para los idiomas de bajo nivel, la mejor práctica es utilizar alguna función de biblioteca inteligente para operaciones complejas en bucle si es posible.

Esas bibliotecas también pueden poner cosas (multiproceso) a sus espaldas y también los programadores especializados las mantienen actualizadas.

Hice un poco más de escrutinio y resultó que en C / C ++, incluso para las operaciones 5e9 = (50,000x100,000), no hay diferencia entre subir y bajar si la prueba se realiza contra una constante como dice @alestanis. (Los resultados de JsPerf a veces son inconsistentes, pero en general dicen lo mismo: no se puede hacer una gran diferencia).
Por lo tanto, --iresulta ser algo "elegante". Solo te hace ver como un mejor programador. :)

Por otro lado, para desenrollarme en esta situación de 5e9, me ha bajado de 12 segundos a 2.5 segundos cuando pasé 10 segundos, y a 2.1 segundos cuando pasé 20 años. Fue sin optimización, y la optimización ha reducido las cosas a poco tiempo inconmensurable. :) (El desenrollado se puede hacer en mi forma anterior o usando i++, pero eso no adelanta las cosas en JavaScript).

En general: mantenga i--/ i++y ++i/ i++diferencias en las entrevistas de trabajo, cumpla array.forEachu otras funciones complejas de la biblioteca cuando estén disponibles. ;)


18
La palabra clave es "puede ser". Su ciclo desenrollado también podría ser más lento que el original. Al optimizar, siempre mida para saber exactamente qué impacto tuvieron sus cambios.
jalf

1
@jalf es cierto, +1. Las diferentes longitudes de desprendimiento (> = 1) son diferentes en eficiencia. Es por eso que es más conveniente dejar este trabajo a las bibliotecas si es posible, sin mencionar que los navegadores se ejecutan en diferentes arquitecturas, por lo que sería mejor si deciden cómo hacerlo array.each(...). No creo que intenten experimentar con la liberación de bucles for simples.
Barney

1
@BarnabasSzabolcs La pregunta es específicamente para JavaScript, no para C u otros lenguajes. En JS no es una diferencia, por favor ver mi respuesta a continuación. Aunque no es aplicable a la pregunta, ¡+1 buena respuesta!
AMK

1
Y ahí lo tienes, +1para conseguirte una Great Answerinsignia. Muy buena respuesta. :)
Rohit Jain

1
APL / A ++ tampoco es un lenguaje. Las personas los usan para expresar que no están interesados ​​en los detalles del idioma, sino en algo que comparten esos idiomas.
Barney

33

i-- es tan rápido como i++

Este código a continuación es tan rápido como el suyo, pero usa una variable adicional:

var up = Things.length;
for (var i = 0; i < up; i++) {
    Things[i]
};

La recomendación es NO evaluar el tamaño de la matriz cada vez. Para grandes matrices se puede ver la degradación del rendimiento.


66
Estás completamente equivocado aquí. Ahora el control de bucle necesita una variable interna adicional (i y superior) y, dependiendo del motor de JavaScript, esto puede limitar el potencial para optimizar el código. Los JIT traducirán bucles simples a códigos de operación de máquina directos, pero si los bucles tienen demasiadas variables, entonces JIT no podrá optimizar tan bien. En general, está limitado por la arquitectura o los registros de CPU que utiliza JIT. Inicializar una vez y bajar simplemente la solución más limpia y es por eso que se recomienda en todo el lugar.
Pavel P

El optimizador de un intérprete / compilador común eliminará la variable adicional. El punto es el 'comparar con 0': vea mi respuesta para obtener más explicaciones.
H.-Dirk Schmitt

1
Es mejor poner 'arriba' dentro del bucle:for (var i=0, up=Things.length; i<up; i++) {}
thdoan

22

Dado que está interesado en el tema, eche un vistazo a la publicación del blog de Greg Reimer sobre un punto de referencia de bucle de JavaScript, ¿Cuál es la forma más rápida de codificar un bucle en JavaScript? :

Creé un conjunto de pruebas de evaluación comparativa de bucles para diferentes formas de codificar bucles en JavaScript. Ya hay algunos de estos, pero no encontré ninguno que reconociera la diferencia entre las matrices nativas y las colecciones HTML.

También puede hacer una prueba de rendimiento en un bucle abriendo https://blogs.oracle.com/greimer/resource/loop-test.html(no funciona si JavaScript está bloqueado en el navegador por, por ejemplo, NoScript ).

EDITAR:

Un punto de referencia más reciente creado por Milan Adamovsky se puede realizar en tiempo de ejecución aquí para diferentes navegadores.

Para una prueba en Firefox 17.0 en Mac OS X 10.6 , obtuve el siguiente bucle:

var i, result = 0;
for (i = steps - 1; i; i--) {
  result += i;
}

como el más rápido precedido por:

var result = 0;
for (var i = steps - 1; i >= 0; i--) {
  result += i;
}

Ejemplo de referencia.


55
Esa publicación tiene 4 años ahora. Dada la velocidad (¡ja!) A la que se han actualizado los motores de JavaScript, es probable que la mayoría de los consejos sean obsoletos: el artículo ni siquiera menciona Chrome y es un brillante motor V8 de JavaScript.
Yi Jiang

Sí, extraño ese detalle, gracias por señalarlo. Evento en ese momento no había mucha diferencia entre --i o i ++ en el ciclo para.
dreamcrash

19

No es el --o ++, es la operación de comparación. Con --puedes usar una comparación con 0, mientras que con ++necesitas compararlo con la longitud. En el procesador, comparar con cero está normalmente disponible, mientras que comparar con un entero finito requiere una resta.

a++ < length

en realidad se compila como

a++
test (a-length)

Por lo tanto, lleva más tiempo en el procesador cuando se compila.


15

Respuesta corta

Para el código normal, especialmente en un lenguaje de alto nivel como JavaScript , no hay diferencia de rendimiento en i++y i--.

El criterio de rendimiento es el uso en el forciclo y la declaración de comparación .

Esto se aplica a todos los lenguajes de alto nivel y es principalmente independiente del uso de JavaScript. La explicación es el código de ensamblador resultante en la línea inferior.

Explicación detallada

Una diferencia de rendimiento puede ocurrir en un bucle. El trasfondo es que en el nivel del código del ensamblador puede ver que a compare with 0es solo una declaración que no necesita un registro adicional.

Esta comparación se emite en cada paso del ciclo y puede resultar en una mejora del rendimiento medible.

for(var i = array.length; i--; )

será evaluado a un pseudo código como este:

 i=array.length
 :LOOP_START
 decrement i
 if [ i = 0 ] goto :LOOP_END
 ... BODY_CODE
 :LOOP_END

Tenga en cuenta que 0 es un literal, o en otras palabras, un valor constante.

for(var i = 0 ; i < array.length; i++ )

será evaluado a un pseudocódigo como este (se supone que la optimización del intérprete normal):

 end=array.length
 i=0
 :LOOP_START
 if [ i < end ] goto :LOOP_END
 increment i
 ... BODY_CODE
 :LOOP_END

Tenga en cuenta que end es una variable que necesita un registro de CPU . Esto puede invocar un intercambio de registro adicional en el código y necesita una declaración de comparación más costosa en la ifdeclaración.

Solo mis 5 centavos

Para un lenguaje de alto nivel, la legibilidad, que facilita el mantenimiento, es más importante como una mejora de rendimiento menor.

Normalmente, la iteración clásica de principio a fin de matriz es mejor.

La iteración más rápida desde el final de la matriz al inicio da como resultado la secuencia inversa posiblemente no deseada.

Post scriptum

Como se preguntó en un comentario: La diferencia de --iy i--está en la evaluación de iantes o después de la disminución.

La mejor explicación es probarlo ;-) Aquí hay un ejemplo de Bash .

 % i=10; echo "$((--i)) --> $i"
 9 --> 9
 % i=10; echo "$((i--)) --> $i"
 10 --> 9

1
1+ Buena explicación. Solo una pregunta un poco fuera de alcance, ¿podría explicar la diferencia entre --iy i--también?
Afshin Mehrabani

14

He visto la misma recomendación en Sublime Text 2.

Como ya se dijo, la mejora principal no es evaluar la longitud de la matriz en cada iteración en el bucle for. Esta es una técnica de optimización bien conocida y particularmente eficiente en JavaScript cuando la matriz es parte del documento HTML (haciendo un forpara todos los lielementos).

Por ejemplo,

for (var i = 0; i < document.getElementsByTagName('li').length; i++)

es mucho más lento que

for (var i = 0, len = document.getElementsByTagName('li').length; i < len; i++)

Desde mi punto de vista, la principal mejora en el formulario en su pregunta es el hecho de que no declara una variable adicional ( lenen mi ejemplo)

Pero si me preguntas, no se trata de la optimización i++vs i--, sino de no tener que evaluar la longitud de la matriz en cada iteración (puedes ver una prueba de referencia en jsperf ).


1
Tengo que hacer una excepción a la palabra que calcula aquí. Vea mi comentario sobre la respuesta de Pavel . La especificación ECMA establece que la longitud de la matriz no se calcula cuando se hace referencia a ella.
kojiro

¿'Evaluar' sería una mejor opción? Dato interesante por cierto, no lo sabía
BBog

El optimizador de un intérprete / compilador común eliminará la variable adicional. Lo mismo se aplica a la evaluación array.length. El punto es el 'comparar con 0': vea mi respuesta para obtener más explicaciones.
H.-Dirk Schmitt

@ H.-DirkSchmitt, aparte de su respuesta de sentido común, durante mucho tiempo en la historia de JavaScript, el compilador no optimizó el costo de rendimiento de la cadena de búsqueda. AFAIK V8 fue el primero en intentarlo.
kojiro

@ H.-DirkSchmitt, tal como dijo Kojiro, este es un truco bien conocido y bien establecido. Incluso si ya no es relevante en los navegadores modernos, eso aún no lo hace obsoleto. Además, hacerlo introduciendo una nueva variable de longitud o con el truco en la pregunta de OP, sigue siendo la mejor manera, en mi opinión. Es algo inteligente y una buena práctica, no creo que tenga nada que ver con el hecho de que el compilador fue optimizado para encargarse de algo que a menudo se hace mal en JS
BBog

13

No creo que tenga sentido decir que i--es más rápido que i++en JavaScript.

En primer lugar , depende totalmente de la implementación del motor de JavaScript.

En segundo lugar , siempre que las construcciones más simples se JIT'E y traduzcan a instrucciones nativas, luego i++vsi-- dependerá totalmente de la CPU que lo ejecute. Es decir, en los ARM (teléfonos móviles) es más rápido bajar a 0 ya que la disminución y la comparación con cero se ejecutan en una sola instrucción.

Probablemente, pensaste que uno era más malo que el otro porque la forma sugerida es

for(var i = array.length; i--; )

pero la forma sugerida no es porque uno sea más rápido que el otro, sino simplemente porque si escribes

for(var i = 0; i < array.length; i++)

luego en cada iteración array.length tenía que ser evaluada (un motor JavaScript más inteligente tal vez podría descubrir que el bucle no cambiará la longitud de la matriz). A pesar de que parece una declaración simple, en realidad es una función que el motor de JavaScript llama bajo el capó.

La otra razón, por qué i--podría considerarse "más rápido" es porque el motor de JavaScript necesita asignar solo una variable interna para controlar el bucle (variable al var i). Si lo comparó con array.length o con alguna otra variable, entonces tenía que haber más de una variable interna para controlar el ciclo, y el número de variables internas es un activo limitado de un motor de JavaScript. Cuantas menos variables se usen en un bucle, más posibilidades tiene JIT de optimización. Es por esoi-- podría considerarse más rápido ...


3
Probablemente valga la pena redactar cuidadosamente cómo array.lengthse evalúa. La longitud no se calcula cuando se refiere a ella. (Es solo una propiedad que se establece cada vez que se crea o cambia un índice de matriz ). Cuando hay un costo adicional, es porque el motor JS no ha optimizado la cadena de búsqueda para ese nombre.
kojiro

Bueno, no estoy seguro de lo que dice la especificación Ecma, pero conocer algunas partes internas de diferentes motores de JavaScript no es simple getLength(){return m_length; }porque hay algo de limpieza involucrada. Pero si intenta pensar al revés: sería bastante ingenioso escribir una implementación de matriz donde la longitud debería calcularse realmente :)
Pavel P

La especificación ECMA requiere que la propiedad de longitud ya esté calculada. El lengthdebe actualizarse inmediatamente cuando se añade o se cambia una propiedad-que-es-un-array-index.
kojiro

Lo que intento decir es que es bastante difícil violar la especificación si intentas pensarlo.
Pavel P

1
x86 es como ARM en ese sentido. dec/jnzvs inc eax / cmp eax, edx / jne.
Peter Cordes

13

Como ninguna de las otras respuestas parece responder a su pregunta específica (más de la mitad de ellas muestran ejemplos de C y discuten lenguajes de nivel inferior, su pregunta es para JavaScript) Decidí escribir la mía.

Entonces, aquí tienes:

Respuesta simple: i-- generalmente es más rápido porque no tiene que ejecutar una comparación con 0 cada vez que se ejecuta, los resultados de las pruebas en varios métodos están a continuación:

Resultados de la prueba: como "probado" por este jsPerf, arr.pop()es en realidad el ciclo más rápido con diferencia. Sin embargo, se centra en --i, i--, i++y++i como lo pidió en su pregunta, aquí están jsPerf (que son de múltiples jsPerf de, consulte fuentes abajo) resultados resumidos:

--iy i--son iguales en Firefox mientras que i--es más rápido en Chrome.

En Chrome, un bucle básico para ( for (var i = 0; i < arr.length; i++)) es más rápido que, i--y --imientras que en Firefox es más lento.

En Chrome y Firefox un caché arr.length es significativamente más rápido con Chrome por delante en aproximadamente 170,000 operaciones / seg.

Sin una diferencia significativa, ++ies más rápido quei++ en la mayoría de los navegadores, AFAIK, nunca es al revés en ningún navegador.

Resumen más corto: arr.pop() es el ciclo más rápido con diferencia; para los bucles específicamente mencionados,i-- es el bucle más rápido.

Fuentes: http://jsperf.com/fastest-array-loops-in-javascript/15 , http://jsperf.com/ipp-vs-ppi-2

Espero que esto responda tu pregunta.


1
Parece que su popprueba parece bastante rápida porque está reduciendo el tamaño de la matriz a 0 para la mayoría del ciclo, por lo que puedo decir. Sin embargo, para asegurarme de que las cosas sean justas, he diseñado este jsperf para crear la matriz de la misma manera con cada prueba, que parece .shift()ser el ganador de mis pocos navegadores, aunque no es lo que esperaba :) jsperf.com / compare-different-types-of-looping
Pebbl

Votado por ser el único en mencionar ++i: D
jaggedsoft

12

Depende de la ubicación de su matriz en la memoria y la proporción de aciertos de las páginas de memoria mientras accede a esa matriz.

En algunos casos, el acceso a los miembros de la matriz en el orden de las columnas es más rápido que el orden de las filas debido al aumento en la proporción de aciertos.


1
Si tan solo el OP hubiera preguntado por qué atravesar la misma matriz en diferentes órdenes puede tomar diferentes cantidades de tiempo ..
dcow

1
Teniendo en cuenta que en los sistemas operativos que su gestión de memoria se basa en la paginación, cuando un proceso necesita datos que no están en páginas en caché, se produce un error de página en el sistema operativo y debe llevar la página de destino a la memoria caché de la CPU y reemplazarla con otra página, por lo tanto, provoca una sobrecarga en el procesamiento que lleva más tiempo que cuando la página de destino está en la memoria caché de la CPU. Supongamos que definimos una matriz grande en la que cada fila es más grande que el tamaño del sistema operativo de la página y accedemos a ella en orden de fila, en este caso la relación de fallas de la página aumenta y el resultado es más lento que el acceso de orden de columna a esa matriz.
Akbar Bayati

Los fallos de página no son lo mismo que los errores de caché. Solo busca errores si su memoria se paginó en el disco. El acceso a las matrices multidimensionales en orden secuencial de cómo se almacenan en la memoria es más rápido debido a la localidad de caché (utilizando todos los bytes de una línea de caché cuando se recupera), no debido a fallas de página. (a menos que su conjunto de trabajo sea demasiado grande para la computadora que está usando.)
Peter Cordes

10

La última vez que me molesté fue cuando escribí el ensamblaje 6502 (¡8 bits, sí!). La gran ganancia es que la mayoría de las operaciones aritméticas (especialmente los decrementos) actualizan un conjunto de indicadores, uno de ellos era Zel indicador de "cero alcanzado".

Entonces, al final del ciclo, acabas de hacer dos instrucciones: DEC(decrementar) y JNZ(saltar si no es cero), ¡no se necesita comparación!


En el caso de JavaScript, obviamente no se aplica (ya que se ejecuta en CPU que no tienen tales códigos operativos). Lo más probable es que la verdadera razón detrás del i--vs i++es que con el primero no se introducen variables de control adicionales en el alcance del bucle. Vea mi respuesta a continuación ...
Pavel P

1
correcto, realmente no aplica; pero es un estilo C muy común, y se ve más limpio para aquellos de nosotros que nos acostumbramos a eso. :-)
Javier

x86 y ARM son como 6502 a este respecto. dec/jnzen lugar de inc/cmp/jnepara el caso x86. No verá un ciclo vacío correr más rápido (ambos saturarán el rendimiento de la rama), pero la cuenta regresiva reduce ligeramente la sobrecarga del ciclo. Los buscadores actuales de Intel HW tampoco se molestan por los patrones de acceso a la memoria que van en orden descendente, tampoco. Las CPU más antiguas creo que podrían rastrear como 4 transmisiones hacia atrás y 6 o 10 transmisiones hacia adelante, IIRC.
Peter Cordes

7

Puede explicarse mediante JavaScript (y todos los idiomas) que eventualmente se convierten en códigos de operación para ejecutarse en la CPU. Las CPU siempre tienen una sola instrucción para comparar contra cero, que es muy rápido.

Por otro lado, si puede garantizar countque siempre es así >= 0, puede simplificar para:

for (var i = count; i--;)
{
  // whatever
}

1
Un código fuente más corto no necesariamente significa que será más rápido. ¿Lo has medido?
Greg Hewgill

Vaya, me perdí este. Clavo en la cabeza hay cap.
Robert

1
Me gustaría ver las fuentes de ensamblaje donde comparar con 0 es diferente. Han pasado algunos años, pero en un momento hice un montón de codificación de ensamblaje y no puedo por mi vida pensar en una forma de hacer una comparación / prueba contra 0 de una manera que no pueda ser igual de rápida para cualquier otro entero. Sin embargo, lo que dices suena cierto. Frustrado porque no puedo entender por qué!
Brian Knoblauch

77
@Brian Knoblauch: Si usa una instrucción como "dec eax" (código x86), esa instrucción establece automáticamente el indicador Z (cero) que puede probar inmediatamente sin tener que usar otra instrucción de comparación en el medio.
Greg Hewgill

1
Sin embargo, al interpretar Javascript, dudo que los códigos de operación sean el cuello de botella. Es más probable que haya menos tokens, lo que significa que el intérprete puede procesar el código fuente más rápido.
Todd Owen

6

Esto no depende del signo --o ++, pero depende de las condiciones que aplique en el bucle.

Por ejemplo: su ciclo es más rápido si la variable tiene un valor estático que si su ciclo verifica las condiciones cada vez, como la longitud de una matriz u otras condiciones.

Pero no se preocupe por esta optimización, porque esta vez su efecto se mide en nanosegundos.


múltiples bucles de nanosegundos pueden convertirse en segundos ... nunca es una mala idea optimizar cuando tienes tiempo para hacerlo
Buksy

6

for(var i = array.length; i--; )No es mucho más rápido. Pero cuando reemplaza array.lengthcon super_puper_function(), eso puede ser significativamente más rápido (ya que se llama en cada iteración). Esa es la diferencia.

Si va a cambiarlo en 2014, no necesita pensar en la optimización. Si va a cambiarlo con "Buscar y reemplazar", no necesita pensar en la optimización. Si no tiene tiempo, no necesita pensar en la optimización. Pero ahora, tienes tiempo para pensarlo.

PD: i--no es más rápido que i++.


6

Para abreviar: no hay absolutamente ninguna diferencia en hacer esto en JavaScript.

En primer lugar, puede probarlo usted mismo:

No solo puede probar y ejecutar cualquier secuencia de comandos en cualquier biblioteca de JavaScript, sino que también tiene acceso a la gran cantidad de secuencias de comandos escritas previamente, así como la capacidad de ver las diferencias entre el tiempo de ejecución en diferentes navegadores en diferentes plataformas.

Por lo que puedes ver, no hay diferencia entre el rendimiento en ningún entorno.

Si desea mejorar el rendimiento de su script, cosas que puede intentar hacer:

  1. Tenga una var a = array.length;declaración para que no calcule su valor cada vez en el ciclo
  2. Desenrollar bucle http://en.wikipedia.org/wiki/Loop_unwinding

Pero debe comprender que la mejora que puede obtener será tan insignificante que, sobre todo, no debería preocuparse por ello.

Mi propia opinión de por qué apareció una idea tan errónea (Dec vs Inc)

Hace mucho, mucho tiempo había una instrucción de máquina común, DSZ (Decremento y Salto en cero). Las personas que programaron en lenguaje ensamblador utilizaron esta instrucción para implementar bucles con el fin de guardar un registro. Ahora, estos hechos antiguos son obsoletos, y estoy bastante seguro de que no obtendrá ninguna mejora de rendimiento en ningún idioma utilizando esta pseudo mejora.

Creo que la única forma en que dicho conocimiento puede propagarse en nuestro tiempo es cuando lees el código de otra persona. Vea tal construcción y pregunte por qué se implementó y aquí la respuesta: "mejora el rendimiento porque se compara con cero". Te sorprendiste de un mayor conocimiento de tu colega y piensas usarlo para ser mucho más inteligente :-)


Interesante, pero para sus pruebas que se ejecutan en Firefox 16.0.2 en win7, el ciclo de disminución fue un 29% más lento ... EDITAR: no tenga en cuenta eso. Las pruebas repetidas no resultaron concluyentes, hay una sorprendente cantidad de ruido en los resultados de mi prueba para las siguientes ejecuciones. No estoy muy seguro de por qué.

Sí, intenté dar cuenta de eso cerrando todo lo demás y simplemente ejecutando las pruebas. Todavía obtuve resultados inestables. Muy extraño.

Creo que te perdiste el punto real de por qué ir a cero se considera mejor en JavaScript. Esto se debe principalmente a que de esta manera solo una variable controla la ejecución del bucle, por ejemplo, optimizador / JITer tiene más margen de mejora. El uso array.lengthno necesariamente conlleva una penalización de rendimiento, simplemente porque la máquina virtual JS es lo suficientemente inteligente como para descubrir si la matriz no es modificada por el cuerpo del bucle. Vea mi respuesta a continuación.
Pavel P

1
nitpicking: este hecho antiguo (optimizaciones de lenguaje ensamblador) no es obsoleto, solo arcano. como en no necesita saber a menos que realmente lo haga. :-)
Javier

6

Hice una comparación en jsbench .

Como señaló alestani, una cosa que lleva tiempo en los bucles ascendentes es evaluar, para cada iteración, el tamaño de su matriz. En este bucle:

for ( var i = 1; i <= array.length; i++ )

evalúa .lengthcada vez que incrementa i. En este:

for ( var i = 1, l = array.length; i <= l; i++ )

evalúa .lengthsolo una vez, cuando declara i. En este:

for ( var i = array.length; i--; )

la comparación es implícita, ocurre justo antes de disminuir i, y el código es muy legible. Sin embargo, lo que puede marcar una gran diferencia es lo que se pone dentro del bucle.

Bucle con llamada a la función (definido en otra parte):

for (i = values.length; i-- ;) {
  add( values[i] );
}

Bucle con código en línea:

var sum = 0;
for ( i = values.length; i-- ;) {
  sum += values[i];
}

Si puede insertar su código en línea, en lugar de llamar a una función, sin sacrificar la legibilidad, ¡puede tener un bucle de un orden de magnitud más rápido!


Nota : como el navegador se está volviendo bueno para incluir funciones simples, realmente depende de qué tan complejo sea su código. Entonces, perfil antes de optimizar, porque

  1. El cuello de botella puede estar en otra parte (ajax, reflujo, ...)
  2. Puedes elegir un mejor algoritmo
  3. Puedes elegir una mejor estructura de datos

Pero recuerda:

El código está escrito para que las personas lo lean, y solo de manera incidental para que las máquinas lo ejecuten.


+1 para esta respuesta y para el punto de referencia. He agregado para cada prueba y rediseñado el punto de referencia a un archivo independiente ejecutable en el navegador, así como en el nodo. jsfiddle.net/hta69may/2 . Para el nodo "Bucle inverso, comparación implícita, código en línea" es el más rápido. Pero las pruebas en FF 50 mostraron resultados curiosos: no solo los tiempos fueron casi 10 veces menos (!) Sino que ambas pruebas "forEach" fueron tan rápidas como el "bucle inverso". ¿Quizás los chicos de Node deberían usar el motor JS de Mozilla en lugar del V8? :)
Fr0sT

4

La forma en que lo estás haciendo ahora no es más rápida (además de ser un ciclo indefinido, supongo que querías hacerlo) i--.

Si quieres hacerlo más rápido, haz lo siguiente:

for (i = 10; i--;) {
    //super fast loop
}

por supuesto, no lo notarías en un bucle tan pequeño. La razón por la que es más rápido es porque está disminuyendo i mientras verifica que es "verdadero" (se evalúa como "falso" cuando llega a 0)


1
¿Te falta un punto y coma? (i = 10; i--;)
searlea

Sí, sí, arreglé el error. ¡Y si vamos a ser exigentes, señalaré que olvidó su punto y coma después de su i--! Je
djdd87

¿Por qué sería eso más rápido? Una fuente más corta no significa que será más rápido. ¿Lo has medido?
Greg Hewgill

44
Sí, lo he medido y no digo que una fuente más corta lo haga más rápido, pero menos operaciones lo hacen más rápido.
peirix

4

A veces, hacer algunos cambios menores en la forma en que escribimos nuestro código puede marcar una gran diferencia en la rapidez con la que nuestro código se ejecuta realmente. Un área donde un cambio de código menor puede hacer una gran diferencia en los tiempos de ejecución es donde tenemos un bucle for que procesa una matriz. Cuando la matriz es de elementos en la página web (como botones de radio), el cambio tiene el mayor efecto, pero vale la pena aplicar este cambio incluso cuando la matriz es interna al código Javascript.

La forma convencional de codificar un bucle for para procesar una matriz es así:

for (var i = 0; i < myArray.length; i++) {...

El problema con esto es que evaluar la longitud de la matriz usando myArray.length lleva tiempo y la forma en que hemos codificado el ciclo significa que esta evaluación debe realizarse cada vez que se realiza el ciclo. Si la matriz contiene 1000 elementos, la longitud de la matriz se evaluará 1001 veces. Si estuviéramos mirando los botones de radio y tuviéramos myForm.myButtons.length, entonces tomará aún más tiempo evaluarlo, ya que el grupo apropiado de botones dentro del formulario especificado debe ubicarse primero antes de que la longitud pueda evaluarse cada vez alrededor del ciclo.

Obviamente, no esperamos que la longitud de la matriz cambie mientras la procesamos, por lo que todos estos nuevos cálculos de la longitud solo se agregan innecesariamente al tiempo de procesamiento. (Por supuesto, si tiene código dentro del bucle que agrega o elimina entradas de matriz, entonces el tamaño de la matriz puede cambiar entre iteraciones y, por lo tanto, no podemos cambiar el código que lo prueba)

Lo que podemos hacer para corregir esto para un ciclo donde el tamaño es fijo es evaluar la longitud una vez al comienzo del ciclo y guardarlo en una variable. Luego podemos probar la variable para decidir cuándo terminar el ciclo. Esto es mucho más rápido que evaluar la longitud de la matriz cada vez, particularmente cuando la matriz contiene más que unas pocas entradas o es parte de la página web.

El código para hacer esto es:

for (var i = 0, var j = myArray.length; i < j; i++) {...

Por lo tanto, ahora solo evaluamos el tamaño de la matriz una vez y probamos nuestro contador de bucle contra la variable que contiene ese valor cada vez alrededor del bucle. Se puede acceder a esta variable adicional mucho más rápido que evaluar el tamaño de la matriz, por lo que nuestro código se ejecutará mucho más rápido que antes. Solo tenemos una variable adicional en nuestro script.

A menudo, no importa en qué orden procesemos la matriz siempre que se procesen todas las entradas de la matriz. Si este es el caso, podemos hacer que nuestro código sea un poco más rápido eliminando la variable adicional que acabamos de agregar y procesando la matriz en orden inverso.

El código final que procesa nuestra matriz de la manera más eficiente posible es:

for (var i = myArray.length-1; i > -1; i--) {...

Este código todavía solo evalúa el tamaño de la matriz una vez al comienzo, pero en lugar de comparar el contador de bucle con una variable, lo comparamos con una constante. Dado que una constante es aún más efectiva para acceder que una variable y dado que tenemos una declaración de asignación menos que antes, nuestra tercera versión del código ahora es ligeramente más eficiente que la segunda versión y mucho más eficiente que la primera.


4

++vs. --no importa porque JavaScript es un lenguaje interpretado, no un lenguaje compilado. Cada instrucción se traduce a más de un lenguaje de máquina y no debe preocuparse por los detalles sangrientos.

Las personas que están hablando de usar --(o ++) para hacer un uso eficiente de las instrucciones de montaje están equivocadas. Estas instrucciones se aplican a la aritmética de enteros y no hay enteros en JavaScript, solo números .

Deberías escribir código legible.


3

En muchos casos, esto no tiene esencialmente nada que ver con el hecho de que los procesadores pueden comparar a cero más rápido que otras comparaciones.

Esto se debe a que solo unos pocos motores Javascript (los que están en la lista JIT) realmente generan código de lenguaje de máquina.

La mayoría de los motores de Javascript crean una representación interna del código fuente que luego interpretan (para tener una idea de cómo es esto, eche un vistazo cerca de la parte inferior de esta página en SpiderMonkey de Firefox ). Generalmente, si un código hace prácticamente lo mismo pero conduce a una representación interna más simple, se ejecutará más rápido.

Tenga en cuenta que con tareas simples como sumar / restar una de una variable, o comparar una variable con algo, la sobrecarga del intérprete que se mueve de una "instrucción" interna a la siguiente es bastante alta, por lo que las "instrucciones" menos son utilizado internamente por el motor JS, mejor.


3

Bueno, no sé acerca de JavaScript, realmente debería ser solo una cuestión de reevaluación de la longitud de la matriz y tal vez algo que ver con las matrices asociativas (si solo disminuye, es poco probable que sea necesario asignar nuevas entradas, si la matriz es densa, es decir, alguien puede optimizar para eso).

En el ensamblaje de bajo nivel, hay una instrucción de bucle, llamada DJNZ (decremento y salto si no es cero). Por lo tanto, el decremento y el salto son todos en una sola instrucción, lo que posiblemente sea un poco más rápido que INC y JL / JB (incremento, salto si es menor que / salto si es inferior). Además, comparar con cero es más simple que comparar con otro número. Pero todo eso es realmente marginal y también depende de la arquitectura de destino (podría hacer la diferencia, por ejemplo, en Arm en un teléfono inteligente).

No esperaría que estas diferencias de bajo nivel tengan un impacto tan grande en los idiomas interpretados, simplemente no he visto DJNZ entre las respuestas, así que pensé en compartir un pensamiento interesante.


Para el registro, DJNZes una instrucción en el 8051 (z80) ISA. x86 tiene en dec/jnzlugar de inc/cmp/jne, y aparentemente arm tiene algo similar. Estoy bastante seguro de que esta no es la causa de la diferencia de Javascript; eso es por tener más para evaluar en la condición de bucle.
Peter Cordes

3

Se solía decir que --i era más rápido (en C ++) porque solo hay un resultado, el valor disminuido. i-- necesita almacenar el valor disminuido nuevamente en i y también conservar el valor original como resultado (j = i--;). En la mayoría de los compiladores, esto usó dos registros en lugar de uno, lo que podría causar que otra variable tenga que escribirse en la memoria en lugar de retenerse como una variable de registro.

Estoy de acuerdo con los otros que han dicho que no hay diferencia en estos días.


3

En palabras muy simples

"i-- e i ++. En realidad, ambos toman el mismo tiempo".

pero en este caso cuando tiene una operación incremental ... el procesador evalúa .length cada vez que la variable se incrementa en 1 y en caso de disminución ... particularmente en este caso, evaluará .length solo una vez hasta que obtengamos 0.


3

Primero, i++y i--tome exactamente el mismo tiempo en cualquier lenguaje de programación, incluido JavaScript.

El siguiente código lleva un tiempo muy diferente.

Rápido:

for (var i = 0, len = Things.length - 1; i <= len; i++) { Things[i] };

Lento:

for (var i = 0; i <= Things.length - 1; i++) { Things[i] };

Por lo tanto, el siguiente código también lleva un tiempo diferente.

Rápido:

for (var i = Things.length - 1; i >= 0; i--) { Things[i] };

Lento:

for (var i = 0; i <= Things.length - 1; i++) { Things[i] };

PS Slow es lento solo para algunos idiomas (motores de JavaScript) debido a la optimización del compilador. La mejor manera es usar '<' en su lugar '<=' (o '=') y '--i' en lugar de 'i--' .


3

I-- o i ++ no consumen mucho tiempo. Si profundiza en la arquitectura de la CPU, ++es más rápido que el --, ya que la --operación hará el complemento de 2, pero sucede dentro del hardware, por lo que será más rápido y no se considerará ninguna diferencia importante entre las operaciones ++y --estas operaciones. menos tiempo consumido en la CPU.

El bucle for se ejecuta así:

  • Inicialice la variable una vez al comienzo.
  • Compruebe la restricción en el segundo operando del circuito, <, >, <=, etc.
  • Luego aplica el bucle.
  • Incremente el ciclo y el ciclo nuevamente arroje estos procesos nuevamente.

Entonces,

for (var i = Things.length - 1; i >= 0; i--) {
    Things[i]
}; 

calculará la longitud de la matriz solo una vez al inicio y esto no es mucho tiempo, pero

for(var i = array.length; i--; ) 

calculará la longitud en cada bucle, por lo que consumirá mucho tiempo.


var i = Things.length - 1; i >= 0; i--calculará la longitud 1 vez también.
Dmitry

1
No estoy seguro de qué significa " --operación hará el complemento de 2", pero supongo que significa que negará algo. No, no niega nada en ninguna arquitectura. Restar 1 es tan simple como sumar 1, solo haces un circuito que toma prestado en lugar de llevar.
Olathe

1

El mejor enfoque para responder a este tipo de preguntas es intentarlo. Configure un bucle que cuente un millón de iteraciones o lo que sea, y hágalo en ambos sentidos. Mida el tiempo de ambos bucles y compare los resultados.

La respuesta probablemente dependerá del navegador que esté utilizando. Algunos tendrán resultados diferentes que otros.


44
Eso todavía no respondería a su pregunta sobre por qué es más rápido. Que acababa de obtener un punto de referencia, sin ningún conocimiento de por qué es así ...
peirix

3
Es verdad. Sin embargo, sin conocer la implementación exacta de cada motor Javascript en cada navegador, es casi imposible responder la parte "por qué". Muchas de las respuestas aquí arrojan recomendaciones anecdóticas como "usar predecremento en lugar de postdecremento" (un truco de optimización de C ++) y "comparar contra cero", lo que podría ser cierto en los lenguajes compilados de código nativo, pero Javascript está muy lejos de ser el elemento básico del UPC.
Greg Hewgill

44
-1 Estoy totalmente en desacuerdo con que el mejor enfoque es intentarlo. Un par de ejemplos no reemplazan el conocimiento colectivo, y ese es el objetivo de foros como este.
Christophe

1

Me encanta, muchas marcas, pero no hay respuesta: D

En pocas palabras, una comparación contra cero siempre es la comparación más rápida

Entonces (a == 0) en realidad es más rápido al devolver True que (a == 5)

Es pequeño e insignificante y con 100 millones de filas en una colección es medible.

es decir, en un bucle hacia arriba, podría estar diciendo donde i <= array.length e incrementar i

en un bucle hacia abajo, podría estar diciendo donde i> = 0 y disminuir en su lugar i.

La comparación es más rápida. No es la "dirección" del bucle.


No hay una respuesta a la pregunta como se indica porque los motores de Javascript son todos diferentes y la respuesta depende exactamente de qué navegador lo está midiendo.
Greg Hewgill

No, es fundamentalmente aceptado que las comparaciones contra cero son las más rápidas. Aunque sus declaraciones también son correctas, la regla de oro de comparar contra cero es absoluta.
Robert

44
Eso es cierto solo si el compilador elige hacer esa optimización, lo que ciertamente no está garantizado. El compilador podría generar exactamente el mismo código para (a == 0) y (a == 5), excepto por el valor de la constante. Una CPU no se comparará con 0 más rápido que con cualquier otro valor, si ambos lados de la comparación son variables (desde el punto de vista de la CPU). En general, solo los compiladores de código nativo tienen la oportunidad de optimizar a este nivel.
Greg Hewgill

1

AYUDE A OTROS A EVITAR UN DOLOR DE CABEZA --- ¡VOTE ESTO!

La respuesta más popular en esta página no funciona para Firefox 14 y no pasa el jsLinter. Los bucles "while" necesitan un operador de comparación, no una asignación. Funciona en cromo, safari e incluso es decir. Pero muere en Firefox.

¡ESTO ESTÁ DESCOMPUESTO!

var i = arr.length; //or 10
while(i--)
{
  //...
}

¡ESTO FUNCIONARÁ! (funciona en firefox, pasa el jsLinter)

var i = arr.length; //or 10
while(i>-1)
{
  //...
  i = i - 1;
}

2
Lo probé en Firefox 14 y funcionó bien. He visto ejemplos de bibliotecas de producción que usan while (i--)y que funcionan en muchos navegadores. ¿Podría haber habido algo extraño en tu prueba? ¿Estaba utilizando una versión beta de Firefox 14?
Matt Browne

1

Esto es solo una suposición, pero tal vez sea porque es más fácil para el procesador comparar algo con 0 (i> = 0) en lugar de con otro valor (i <Things.length).


3
+1 No otros han votado en contra. Aunque la evaluación repetida de .length es mucho más un problema que el inc / decremento real, la verificación del final del ciclo puede ser importante. Mi compilador de C da algunas advertencias sobre bucles como: "comentario # 1544-D: (ULP 13.1) Se detectó el conteo de bucles. Recomendar la cuenta regresiva de bucles ya que detectar ceros es más fácil"
Harper
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.