Muy simple, ¿qué es la optimización de llamadas de cola?
Más específicamente, ¿cuáles son algunos pequeños fragmentos de código en los que se podría aplicar y, en caso contrario, con una explicación de por qué?
Muy simple, ¿qué es la optimización de llamadas de cola?
Más específicamente, ¿cuáles son algunos pequeños fragmentos de código en los que se podría aplicar y, en caso contrario, con una explicación de por qué?
Respuestas:
La optimización de llamada de cola es donde puede evitar asignar un nuevo marco de pila para una función porque la función de llamada simplemente devolverá el valor que obtiene de la función llamada. El uso más común es la recursividad de cola, donde una función recursiva escrita para aprovechar la optimización de llamada de cola puede usar espacio de pila constante.
Scheme es uno de los pocos lenguajes de programación que garantiza en la especificación que cualquier implementación debe proporcionar esta optimización (JavaScript también lo hace, comenzando con ES6) , así que aquí hay dos ejemplos de la función factorial en Scheme:
La primera función no es recursiva de cola porque cuando se realiza la llamada recursiva, la función necesita realizar un seguimiento de la multiplicación que necesita hacer con el resultado después de que la llamada regresa. Como tal, la pila tiene el siguiente aspecto:
Por el contrario, el seguimiento de la pila para el factor recursivo de cola se ve de la siguiente manera:
Como puede ver, solo necesitamos hacer un seguimiento de la misma cantidad de datos para cada llamada a la cola de hechos porque simplemente estamos devolviendo el valor que obtenemos a la cima. Esto significa que incluso si tuviera que llamar (hecho 1000000), solo necesito la misma cantidad de espacio que (hecho 3). Este no es el caso con el hecho no recursivo de la cola, y como tales valores grandes pueden causar un desbordamiento de la pila.
fuente
Veamos un ejemplo simple: la función factorial implementada en C.
Comenzamos con la definición recursiva obvia
Una función termina con una llamada de cola si la última operación antes de que la función regrese es otra llamada de función. Si esta llamada invoca la misma función, es recursiva de cola.
A pesar
fac()
de que a primera vista parece recursiva, no es que lo que realmente sucede eses decir, la última operación es la multiplicación y no la llamada a la función.
Sin embargo, es posible reescribir
fac()
para ser recursivo de cola pasando el valor acumulado hacia abajo en la cadena de llamadas como un argumento adicional y pasando solo el resultado final nuevamente como el valor de retornoAhora, ¿por qué es útil? Como volvemos inmediatamente después de la llamada de cola, podemos descartar el stackframe anterior antes de invocar la función en posición de cola o, en caso de funciones recursivas, reutilizar el stackframe tal cual.
La optimización de llamada de cola transforma nuestro código recursivo en
Esto se puede incorporar
fac()
y llegamos aque es equivalente a
Como podemos ver aquí, un optimizador suficientemente avanzado puede reemplazar la recursividad de cola con iteración, lo cual es mucho más eficiente ya que evita la sobrecarga de llamadas de función y solo usa una cantidad constante de espacio de pila.
fuente
TCO (Tail Call Optimization) es el proceso mediante el cual un compilador inteligente puede realizar una llamada a una función y no ocupa espacio adicional en la pila. La única situación en la que esto sucede es si la última instrucción ejecutada en una función f es una llamada a una función g (Nota: g puede ser f ). La clave aquí es que f ya no necesita espacio en la pila: simplemente llama a gy luego devuelve lo que g devolvería. En este caso, se puede hacer la optimización de que g simplemente se ejecuta y devuelve cualquier valor que tenga para lo que se llama f.
Esta optimización puede hacer que las llamadas recursivas tomen un espacio de pila constante, en lugar de explotar.
Ejemplo: esta función factorial no es TCOptimizable:
Esta función hace cosas además de llamar a otra función en su declaración de retorno.
La siguiente función es TCOptimizable:
Esto se debe a que lo último que sucede en cualquiera de estas funciones es llamar a otra función.
fuente
(cons a (foo b))
o(+ c (bar d))
en posición de cola de la misma manera.Probablemente la mejor descripción de alto nivel que he encontrado para llamadas de cola, llamadas de cola recursivas y optimización de llamadas de cola es la publicación del blog
"Qué diablos es: una llamada de cola"
por Dan Sugalski. Sobre la optimización de la llamada de cola, escribe:
Y en la recursividad de la cola:
Para que esto:
se convierte silenciosamente en:
Lo que me gusta de esta descripción es lo sucinto y fácil que es comprender a aquellos que provienen de un entorno de lenguaje imperativo (C, C ++, Java)
fuente
foo
función inicial no está optimizada? Solo está llamando a una función como su último paso, y simplemente está devolviendo ese valor, ¿verdad?Tenga en cuenta en primer lugar que no todos los idiomas lo admiten.
El TCO se aplica a un caso especial de recursión. Lo esencial es que, si lo último que hace en una función es llamarse a sí mismo (por ejemplo, se llama a sí mismo desde la posición de "cola"), esto puede ser optimizado por el compilador para actuar como iteración en lugar de recursión estándar.
Verá, normalmente durante la recursividad, el tiempo de ejecución necesita realizar un seguimiento de todas las llamadas recursivas, de modo que cuando uno regrese pueda reanudarse en la llamada anterior, y así sucesivamente. (Intente escribir manualmente el resultado de una llamada recursiva para tener una idea visual de cómo funciona). Hacer un seguimiento de todas las llamadas ocupa espacio, lo que se vuelve significativo cuando la función se llama mucho a sí misma. Pero con TCO, solo puede decir "volver al principio, solo que esta vez cambiar los valores de los parámetros a estos nuevos". Puede hacerlo porque nada después de la llamada recursiva se refiere a esos valores.
fuente
foo
método inicial no está optimizado?Ejemplo de ejecución mínima de GCC con análisis de desmontaje x86
Veamos cómo GCC puede hacer automáticamente las optimizaciones de llamadas de cola mirando el ensamblaje generado.
Esto servirá como un ejemplo extremadamente concreto de lo que se mencionó en otras respuestas como https://stackoverflow.com/a/9814654/895245 de que la optimización puede convertir las llamadas de funciones recursivas en un bucle.
Esto a su vez ahorra memoria y mejora el rendimiento, ya que los accesos a la memoria son a menudo lo principal que hace que los programas sean lentos hoy en día .
Como entrada, le damos a GCC un factorial basado en una pila ingenua no optimizada:
tail_call.c
GitHub aguas arriba .
Compilar y desmontar:
donde
-foptimize-sibling-calls
es el nombre de generalización de las llamadas de cola de acuerdo conman gcc
:como se menciona en: ¿Cómo verifico si gcc está realizando la optimización de recursión de cola?
Elijo
-O1
porque:-O0
. Sospecho que esto se debe a que faltan las transformaciones intermedias requeridas.-O3
produce un código impío y eficiente que no sería muy educativo, aunque también está optimizado.Desmontaje con
-fno-optimize-sibling-calls
:Con
-foptimize-sibling-calls
:La diferencia clave entre los dos es que:
los
-fno-optimize-sibling-calls
usoscallq
, que es la llamada de función típica no optimizada.Esta instrucción empuja la dirección de retorno a la pila, por lo tanto, la aumenta.
Además, esta versión también lo hace
push %rbx
, lo que empuja%rbx
a la pila .GCC hace esto porque almacena
edi
, que es el primer argumento de función (n
)ebx
, luego llamafactorial
.GCC necesita hacer esto porque se está preparando para otra llamada a
factorial
, que utilizará la nuevaedi == n-1
.Elige
ebx
porque este registro está guardado por la persona que llama: los registros se conservan a través de una llamada a la función linux x86-64 para que la llamada secundaria afactorial
no lo cambie y pierdan
.el
-foptimize-sibling-calls
no utiliza ningún instrucciones que empujan a la pila: sólo lo hacegoto
salta dentrofactorial
de las instruccionesje
yjne
.Por lo tanto, esta versión es equivalente a un ciclo while, sin ninguna llamada a la función. El uso de la pila es constante.
Probado en Ubuntu 18.10, GCC 8.2.
fuente
Mira aquí:
http://tratt.net/laurie/tech_articles/articles/tail_call_optimization
Como probablemente sepa, las llamadas a funciones recursivas pueden causar estragos en una pila; Es fácil quedarse rápidamente sin espacio de pila. La optimización de llamadas de cola es la forma en que puede crear un algoritmo de estilo recursivo que utiliza espacio de pila constante, por lo tanto, no crece y crece y obtiene errores de pila.
fuente
Deberíamos asegurarnos de que no haya declaraciones de goto en la función en sí misma. Cuidamos que la llamada a la función sea lo último en la función de llamada.
Las recursiones a gran escala pueden usar esto para optimizaciones, pero en pequeña escala, la sobrecarga de instrucciones para hacer que la función llame una llamada de cola reduce el propósito real.
El TCO puede causar una función que se ejecuta para siempre:
fuente
El enfoque de la función recursiva tiene un problema. Construye una pila de llamadas de tamaño O (n), lo que hace que nuestra memoria total cueste O (n). Esto lo hace vulnerable a un error de desbordamiento de pila, donde la pila de llamadas se vuelve demasiado grande y se queda sin espacio.
Esquema de optimización de llamada de cola (TCO). Donde puede optimizar las funciones recursivas para evitar la acumulación de una gran pila de llamadas y, por lo tanto, ahorra el costo de la memoria.
Hay muchos lenguajes que están haciendo TCO como (JavaScript, Ruby y algunos C) mientras que Python y Java no hacen TCO.
El lenguaje JavaScript ha confirmado usando :) http://2ality.com/2015/06/tail-call-optimization.html
fuente
En un lenguaje funcional, la optimización de la llamada de cola es como si una llamada de función pudiera devolver una expresión parcialmente evaluada como resultado, que luego sería evaluada por la persona que llama.
f 6 se reduce a g 6. Entonces, si la implementación pudiera devolver g 6 como resultado, y luego llamar a esa expresión, guardaría un marco de pila.
también
Reduce a f 6 a g 6 o h 6. Entonces, si la implementación evalúa c 6 y encuentra que es cierto, entonces puede reducir,
Un simple intérprete de optimización de llamada no final podría verse así,
Un intérprete de optimización de llamada de cola podría verse así,
fuente