Lenguaje C
En el lenguaje de programación C, es fácil tener una recursión de cola :
int foo(...) {
return foo(...);
}
Simplemente regrese como es el valor de retorno de la llamada recursiva. Es especialmente importante cuando esta recursión puede repetirse mil o incluso un millón de veces. Usaría mucha memoria en la pila .
Oxido
Ahora, tengo una función Rust que podría llamarse recursivamente un millón de veces:
fn read_all(input: &mut dyn std::io::Read) -> std::io::Result<()> {
match input.read(&mut [0u8]) {
Ok ( 0) => Ok(()),
Ok ( _) => read_all(input),
Err(err) => Err(err),
}
}
(este es un ejemplo mínimo, el real es más complejo, pero captura la idea principal)
Aquí, el valor de retorno de la llamada recursiva se devuelve tal cual, pero:
¿Garantiza que el compilador Rust aplicará una recursión de cola?
Por ejemplo, si declaramos alguna variable que necesita ser destruida como a std::Vec
, ¿será destruida justo antes de la llamada recursiva (que permite la recursión de cola) o después de que la llamada recursiva regrese (que prohíbe la recursividad de cola)?
Respuestas:
Las llamadas de cola están garantizadas siempre que se llame a su función recursiva en posición de cola (básicamente la última declaración de la función).
Rust nunca garantiza la optimización de llamadas de cola , aunque el optimizador puede optar por realizarla.
Tengo entendido que este es uno de los puntos conflictivos, ya que cambiar la ubicación de las variables de pila destruidas sería polémico.
Ver también:
fuente
La respuesta de Shepmaster explica que la optimización de llamadas de cola, que prefiero llamar eliminación de llamadas de cola, no está garantizada en Rust. ¡Pero esa no es toda la historia! Hay muchas posibilidades entre "nunca sucede" y "garantizado". Echemos un vistazo a lo que hace el compilador con un código real.
¿Sucede en esta función?
A partir de ahora, la última versión de Rust disponible en Compiler Explorer es 1.39, y no elimina la llamada de cola
read_all
.Observe esta línea:
call qword ptr [rip + example::read_all@GOTPCREL]
. Esa es la llamada recursiva. Como se puede ver por su existencia, no fue eliminado.Compare esto con una función equivalente con un explícito
loop
:que no tiene una llamada de cola para eliminar, y por lo tanto compila a una función con solo una
call
(a la dirección calculada deinput.read
).Oh bien. Quizás Rust no sea tan bueno como C. ¿O sí?
¿Sucede en C?
Aquí hay una función recursiva de cola en C que realiza una tarea muy similar:
Esto debería ser súper fácil de eliminar para el compilador. La llamada recursiva está justo en la parte inferior de la función y C no tiene que preocuparse por ejecutar destructores. Pero, sin embargo, está esa llamada recursiva de cola , molestamente no eliminada:
Resulta que la optimización de la cola de llamadas tampoco está garantizada en C. Probé Clang y gcc bajo diferentes niveles de optimización, pero nada de lo que intenté convertiría esta función recursiva bastante simple en un bucle.
¿ Alguna vez pasa?
Bien, entonces no está garantizado. ¿Puede el compilador hacerlo? ¡Si! Aquí hay una función que calcula los números de Fibonacci a través de una función interna recursiva de cola:
No solo se elimina la llamada de cola , sino que
fibonacci_lr
se integra toda la funciónfibonacci
, produciendo solo 12 instrucciones (y nocall
a la vista):Si compara esto con un
while
bucle equivalente , el compilador genera casi el mismo ensamblaje.¿Cuál es el punto de?
Probablemente no debería confiar en las optimizaciones para eliminar las llamadas de cola, ya sea en Rust o en C. Es agradable cuando sucede, pero si necesita asegurarse de que una función se compila en un ciclo cerrado, la forma más segura, al menos para ahora, es usar un bucle.
fuente