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
.
example::read_all:
push r15
push r14
push rbx
sub rsp, 32
mov r14, rdx
mov r15, rsi
mov rbx, rdi
mov byte ptr [rsp + 7], 0
lea rdi, [rsp + 8]
lea rdx, [rsp + 7]
mov ecx, 1
call qword ptr [r14 + 24]
cmp qword ptr [rsp + 8], 1
jne .LBB3_1
movups xmm0, xmmword ptr [rsp + 16]
movups xmmword ptr [rbx], xmm0
jmp .LBB3_3
.LBB3_1:
cmp qword ptr [rsp + 16], 0
je .LBB3_2
mov rdi, rbx
mov rsi, r15
mov rdx, r14
call qword ptr [rip + example::read_all@GOTPCREL]
jmp .LBB3_3
.LBB3_2:
mov byte ptr [rbx], 3
.LBB3_3:
mov rax, rbx
add rsp, 32
pop rbx
pop r14
pop r15
ret
mov rbx, rax
lea rdi, [rsp + 8]
call core::ptr::real_drop_in_place
mov rdi, rbx
call _Unwind_Resume@PLT
ud2
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ícitoloop
:
pub fn read_all(input: &mut dyn std::io::Read) -> std::io::Result<()> {
loop {
match input.read(&mut [0u8]) {
Ok ( 0) => return Ok(()),
Ok ( _) => continue,
Err(err) => return Err(err),
}
}
}
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 de input.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:
int read_all(FILE *input) {
char buf[] = {0, 0};
if (!fgets(buf, sizeof buf, input))
return feof(input);
return read_all(input);
}
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:
call read_all
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:
pub fn fibonacci(n: u64) -> u64 {
fn fibonacci_lr(n: u64, a: u64, b: u64) -> u64 {
match n {
0 => a,
_ => fibonacci_lr(n - 1, a + b, a),
}
}
fibonacci_lr(n, 1, 0)
}
No solo se elimina la llamada de cola , sino que fibonacci_lr
se integra toda la función fibonacci
, produciendo solo 12 instrucciones (y no call
a la vista):
example::fibonacci:
push 1
pop rdx
xor ecx, ecx
.LBB0_1:
mov rax, rdx
test rdi, rdi
je .LBB0_3
dec rdi
add rcx, rax
mov rdx, rcx
mov rcx, rax
jmp .LBB0_1
.LBB0_3:
ret
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.