Rendimiento: recursividad vs. iteración en Javascript

24

Recientemente he leído algunos artículos (por ejemplo, http://dailyjs.com/2012/09/14/functional-programming/ ) sobre los aspectos funcionales de Javascript y la relación entre Scheme y Javascript (este último fue influenciado por el primero, que es un lenguaje funcional, mientras que los aspectos OO se heredan de Self, que es un lenguaje basado en prototipos).

Sin embargo, mi pregunta es más específica: me preguntaba si hay métricas sobre el rendimiento de la recursión frente a la iteración en Javascript.

Sé que en algunos idiomas (donde la iteración de diseño funciona mejor) la diferencia es mínima porque el intérprete / compilador convierte la recursión en iteración, sin embargo, supongo que probablemente este no sea el caso de Javascript ya que es, al menos parcialmente, funcional idioma.

mastazi
fuente
3
Haga su propia prueba y descubra de inmediato en jsperf.com
TehShrike
con la recompensa y una respuesta mencionando TCO. Parece que ES6 especifica TCO, pero hasta ahora nadie lo implementa si creemos que kangax.github.io/compat-table/es6 ¿Me estoy perdiendo algo?
Matthias Kauer

Respuestas:

28

JavaScript no realiza la optimización de recursión de cola , por lo que si su recursión es demasiado profunda, puede obtener un desbordamiento de la pila de llamadas. La iteración no tiene tales problemas. Si cree que va a recurrir demasiado y realmente necesita recurrencia (por ejemplo, para rellenar con inundación), reemplace la recurrencia con su propia pila.

El rendimiento de recursión es probablemente peor que el rendimiento de la iteración, porque las llamadas a funciones y los retornos requieren la preservación y restauración del estado, mientras que la iteración simplemente salta a otro punto en una función.

Triang3l
fuente
Solo me pregunto ... He visto un poco de código donde se crea una matriz vacía y el sitio de función recursiva se asigna a una posición en la matriz y luego se devuelve el valor almacenado en la matriz. ¿Es eso lo que quieres decir con "reemplazar la recursión con tu propia pila"? Por ejemplo: var stack = []; var factorial = function(n) { if(n === 0) { return 1 } else { stack[n-1] = n * factorial(n - 1); return stack[n-1]; } }
mastazi
@mastazi: No, esto hará una pila de llamadas inútil junto con la interna. Me refería a algo como el relleno de inundación basado en colas de Wikipedia .
Triang3l
Vale la pena señalar que un lenguaje no realiza TCO, pero una implementación podría hacerlo. La forma en que las personas optimizan JS significa que quizás el TCO podría aparecer en algunas implementaciones
Daniel Gratzer
1
@mastazi Reemplace el elseen esa función con else if (stack[n-1]) { return stack[n-1]; } elsey tiene memoria . Quien escribió ese código factorial tuvo una implementación incompleta (y probablemente debería haberlo usado en stack[n]todas partes en lugar de stack[n-1]).
Izkata
Gracias @Izkata, a menudo hago ese tipo de optimización, pero hasta hoy no sabía su nombre. Debería haber estudiado CS en lugar de IT ;-)
mastazi
20

Actualización : desde ES2015, JavaScript tiene TCO , por lo que parte del argumento a continuación ya no es válido.


Aunque Javascript no tiene una optimización de llamadas de cola, la recursión es a menudo la mejor manera de hacerlo. Y sinceramente, excepto en casos extremos, no obtendrá desbordamientos de la pila de llamadas.

El rendimiento es algo a tener en cuenta, pero también la optimización prematura. Si crees que la recursividad es más elegante que la iteración, entonces hazlo. Si resulta que este es su cuello de botella (que puede que nunca lo sea), puede reemplazarlo con alguna iteración fea. Pero la mayoría de las veces, el cuello de botella se encuentra en las manipulaciones DOM o, más en general, en las E / S, no en el código en sí.

La recursión siempre es más elegante 1 .

1 : Opinión personal.

Florian Margaine
fuente
3
Estoy de acuerdo en que la recursividad es más elegante, y la elegancia es importante, ya que es legibilidad y mantenibilidad (esto es subjetivo, pero en mi opinión, la recursividad es muy fácil de leer, por lo tanto mantenible). Sin embargo, a veces el rendimiento es importante; ¿Puedes apoyar la afirmación de que la recursividad es la mejor manera de hacerlo, incluso en términos de rendimiento?
mastazi
3
@mastazi como dije en mi respuesta, dudo que la recursión sea tu cuello de botella. La mayoría de las veces, es la manipulación DOM, o más generalmente la E / S. Y no olvide que la optimización prematura es la raíz de todos los males;)
Florian Margaine
¡+1 para que la manipulación DOM sea un cuello de botella la mayoría de las veces! Recuerdo una entrevista muy interesante a Yehuda Katz (Ember.js) sobre esto.
mastazi
1
@mike La definición de "prematuro" es "maduro o maduro antes del tiempo apropiado". Si sabe que hacer algo de forma recursiva provocará un desbordamiento de la pila, entonces no es prematuro. Sin embargo, si asumes por capricho (sin datos reales), entonces es prematuro.
Zirak
2
Con Javascript no sabes cuánta pila tendrá disponible el programa. Podría tener una pila pequeña en IE6 o una pila grande en Firefox. Los algoritmos recursivos rara vez tienen una profundidad fija a menos que esté haciendo un bucle recursivo estilo Scheme. Simplemente no parece que la recursión no basada en bucles encaje para evitar optimizaciones prematuras.
mike30
7

Tenía bastante curiosidad sobre este rendimiento en JavaScript también, así que hice algunos experimentos (aunque en una versión anterior del nodo). Escribí una calculadora factorial de forma recursiva frente a iteraciones y la ejecuté varias veces localmente. El resultado parecía bastante sesgado hacia la recursión con un impuesto (esperado).

Código: https://github.com/j03m/trickyQuestions/blob/master/factorial.js

Result:
j03m-MacBook-Air:trickyQuestions j03m$ node factorial.js 
Time:557
Time:126
j03m-MacBook-Air:trickyQuestions j03m$ node factorial.js 
Time:519
Time:120
j03m-MacBook-Air:trickyQuestions j03m$ node factorial.js 
Time:541
Time:123
j03m-MacBook-Air:trickyQuestions j03m$ node --version
v0.8.22
Dr.HappyPants
fuente
Puede probar esto "use strict";y ver si hace la diferencia. (Producirá jumps en lugar de la secuencia de llamada estándar)
Burdock
1
En una versión reciente del nodo (6.9.1) obtuve resultados extremadamente similares. Hay un pequeño impuesto sobre la recursividad, pero lo considero un caso extremo: la diferencia de 400 ms para 1,000,000 de bucles es de .0025 ms por bucle. Si está haciendo 1,000,000 de bucles, es algo a tener en cuenta.
Kelz
6

Según lo solicitado por el OP , voy a participar (sin hacer el ridículo, con suerte: P)

Creo que todos estamos de acuerdo en que la recursividad es solo una forma más elegante de codificación. Si se hace bien, puede hacer que el código sea más fácil de mantener, lo cual es IMHO tan importante (si no más) que reducir 0.0001ms.

En lo que respecta al argumento de que JS no realiza la optimización de llamadas de cola: eso ya no es del todo cierto, el uso del modo estricto de ECMA5 habilita el TCO. Era algo de lo que no estaba muy contento hace un tiempo, pero al menos ahora sé por qué arguments.calleearroja errores en modo estricto. Sé que el enlace de arriba enlaza con un informe de error, pero el error está configurado en WONTFIX. Además, viene el TCO estándar: ECMA6 (diciembre de 2013).

Instintivamente, y atendiendo a la naturaleza funcional de JS, diría que la recursión es el estilo de codificación más eficiente el 99.99% del tiempo. Sin embargo, Florian Margaine tiene razón cuando dice que es probable que se encuentre el cuello de botella en otro lugar. Si está manipulando el DOM, probablemente esté mejor enfocado en escribir su código lo más fácil de mantener posible. La API DOM es lo que es: lenta.

Creo que es casi imposible ofrecer una respuesta definitiva sobre cuál es la opción más rápida. Últimamente, muchos jspref que he visto muestran que el motor V8 de Chrome es ridículamente rápido en algunas tareas, que funcionan 4 veces más lento en SpiderMonkey de FF y viceversa. Los motores JS modernos tienen todo tipo de trucos bajo la manga para optimizar su código. No soy un experto, pero siento que V8, por ejemplo, está altamente optimizado para cierres (y recursividad), mientras que el motor JScript de MS no lo está. SpiderMonkey a menudo funciona mejor cuando el DOM está involucrado ...

En resumen: diría que la técnica que tendrá más rendimiento es, como siempre en JS, casi imposible de predecir.

Elias Van Ootegem
fuente
3

Sin el modo estricto, el rendimiento de iteración suele ser ligeramente más rápido que la recursividad ( además de hacer que el JIT haga más trabajo ). La optimización de recursión de cola esencialmente elimina cualquier diferencia notable porque convierte toda la secuencia de llamadas en un salto.

Ejemplo: Jsperf

Sugeriría preocuparse mucho más por la claridad y simplicidad del código cuando se trata de elegir entre recursividad e iteración.

Bardana
fuente