Cómo determinar si una función recursiva es más eficiente

Para determinar si un algoritmo recursivo o iterativo es más eficiente sin escribir ambas implementaciones y perfilar el código resultante, puede tomar una estimación aproximada del dedo en el viento determinando ciertas medidas de eficiencia del algoritmo teórico (por ejemplo, la notación “O grande”) .

Sin embargo, para hacer esto con mayor precisión, debe comprender algunos detalles sobre la cadena de herramientas y la arquitectura de destino y la plataforma de hardware con la que está trabajando, con resultados que pueden variar enormemente dependiendo de esos detalles.

Uno de los ejemplos más obvios sería que muchos lenguajes (ya sea por especificación o en implementaciones específicas) no implementan la optimización de llamadas de cola. En estos casos, un algoritmo cuya implementación iterativa no requiera el mantenimiento de una pila o lista para mantener el estado (es decir, aquellos que pueden traducirse trivialmente en forma recursiva de llamada de cola) será casi seguro superior a una versión recursiva porque la implementación requerirá creación de un marco de pila para cada llamada recursiva sucesiva, incluso si no se requiere teóricamente.

Las cosas pueden ponerse mucho más difíciles y más difíciles de predecir cuando ciertas clases de optimizaciones del compilador podrían haberse implementado, pero las optimizaciones no son aplicables a una forma u otra porque solo ciertos tipos de estructuras en los AST u otras representaciones intermedias son reconocidas por algoritmos de optimización (la falta de optimización de llamadas de cola a veces es posiblemente un ejemplo específico de esta clase de problema).

Entonces, para saber cuál será mejor sin escribirlos, debe sentirse muy cómodo con la especificación del idioma y los detalles de la implementación específica del idioma con el que está trabajando, y tal vez incluso la configuración / opciones utilizadas para compilar o ejecutar el código. Por ejemplo, si es un código C que se compilará usando gcc con -funroll-loops, podría utilizar totalmente el rendimiento de un algoritmo iterativo si, en su plataforma de destino, el código resultante causa un aumento en la memoria caché de instrucciones como resultado.

En la práctica, si realmente es tan importante determinar si un algoritmo iterativo o recursivo es mejor (suponiendo que ambos logren lo mismo), realmente debería escribir y perfilar ambos.

Editar:
Después de escribir esta respuesta, se me ocurrió que sería casi desmesurado no tener en cuenta que, fuera de los ejemplos extremadamente raros o artificiales, la posibilidad de que las diferencias de eficiencia de rendimiento entre un algoritmo recursivo o iterativo realmente dicte la elección de ir con uno versus el otro es básicamente 0%. Si incluso tiene que hacerse esta pregunta, NO base su decisión en su conjetura sobre cuál será más eficiente. En su lugar, céntrelo en cuál entiende y puede escribir más claramente. Si tiene que hacer esta pregunta “legítimamente”, entonces está realmente más en el ámbito de si debe o no elaborar a mano algún asm en línea o un kernel fortan o C al que se accede a través del FFI de su idioma o algo así. .

Si alguna vez necesita escribir una función recursiva, en realidad tiene dos opciones: escribir la función recursiva o implementar la misma función usando un bucle y una pila. Por ejemplo, supongamos que desea atravesar un árbol binario en orden. Puedes hacerlo con una función recursiva:

def traverse (nodo):
si el nodo es Ninguno:
regreso
transversal (nodo izquierdo)
nodo de impresión
transversal (nodo.derecho)

O bien, puede hacer lo mismo sin recurrir nunca:

def traverse (nodo):
s = pila ()
mientras el nodo no es None y no es s: # Asegúrese de que la pila no esté vacía
mientras el nodo no es Ninguno:
s.push (nodo)
nodo = nodo.izquierda
si el nodo es Ninguno y no s:
nodo = s.pop ()
nodo de impresión
nodo = nodo.derecho

¡Sorprendentemente, estos dos segmentos de código hacen lo mismo! Claramente, el primero es mucho más ordenado. La pregunta es: ¿cuál es más eficiente?

De hecho, ¡ambos segmentos de código tienen la misma eficiencia! Ambos visitan cada nodo del árbol exactamente una vez, por lo que se ejecutan en tiempo O (N), donde N es el número de nodos en el árbol.

Como resultado, cuando se implementan correctamente, las funciones recursivas siempre tienen la misma eficiencia que sus análogos iterativos. Aunque el código recursivo se ve muy diferente, se comporta de la misma manera dentro de la computadora: a medida que se ejecuta, algo llamado pila de llamadas acumula cuadros de pila cada vez que la función va un nivel más profundo, por lo tanto, ambas versiones incluso usan una estructura de datos de pila.

Entonces, ambas versiones tienen la misma eficiencia. Sin embargo, hay una advertencia: dado que la versión recursiva acumula la pila de llamadas, puede bloquearse si hay un límite en el tamaño de la pila de llamadas. Sin embargo, el tamaño de la pila en la versión iterativa está limitado por la cantidad de RAM asignada a su programa. Por esa razón, si bien ambas versiones tienen la misma eficiencia, la versión iterativa generalmente se considera un mejor enfoque.