Linguagem C
Na linguagem de programação C, é fácil ter recursão de cauda :
int foo(...) {
return foo(...);
}
Apenas retorne como é o valor de retorno da chamada recursiva. É especialmente importante quando essa recursão pode se repetir mil ou até um milhão de vezes. Usaria muita memória na pilha .
Ferrugem
Agora, eu tenho uma função Rust que pode se chamar recursivamente um milhão de vezes:
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 é um exemplo mínimo, o real é mais complexo, mas captura a ideia principal)
Aqui, o valor de retorno da chamada recursiva é retornado como está, mas:
Isso garante que o compilador Rust aplique uma recursão de cauda?
Por exemplo, se declararmos alguma variável que precisa ser destruída como a std::Vec
, ela será destruída imediatamente antes da chamada recursiva (que permite recursão de cauda) ou após o retorno da chamada recursiva (que proíbe a recursão de cauda)?
fonte
Respostas:
As chamadas de cauda são garantidas sempre que sua função recursiva é chamada na posição de cauda (basicamente a última declaração da função).
A otimização da chamada de cauda nunca é garantida pelo Rust, embora o otimizador possa optar por executá-la.
Entendo que esse é um dos pontos problemáticos, pois alterar a localização das variáveis de pilha destruídas seria controverso.
Veja também:
fonte
A resposta de Shepmaster explica que a otimização da chamada de cauda, que eu prefiro chamar eliminação de chamada de cauda, não está garantida no Rust. Mas essa não é a história toda! Existem muitas possibilidades entre "nunca acontece" e "garantido". Vamos dar uma olhada no que o compilador faz com algum código real.
Isso acontece nessa função?
No momento, a versão mais recente do Rust disponível no Compiler Explorer é a 1.39 e não elimina a chamada final
read_all
.Observe esta linha:
call qword ptr [rip + example::read_all@GOTPCREL]
. Essa é a ligação recursiva. Como você pode ver por sua existência, não foi eliminado.Compare isso com uma função equivalente com uma explícita
loop
:que não tem chamada final para eliminar e, portanto, compila uma função com apenas uma
call
(no endereço calculado deinput.read
).Ah bem. Talvez Rust não seja tão bom quanto C. Ou é?
Isso acontece em C?
Aqui está uma função recursiva da cauda em C que executa uma tarefa muito semelhante:
Isso deve ser super fácil para o compilador eliminar. A chamada recursiva fica na parte inferior da função e C não precisa se preocupar com a execução de destruidores. Mas, no entanto, existe essa chamada recursiva , irritantemente não eliminada:
Acontece que a otimização da chamada de cauda também não é garantida em C. Eu tentei o Clang e o gcc sob diferentes níveis de otimização, mas nada que eu tentasse transformaria essa função recursiva bastante simples em um loop.
Isso já aconteceu?
Ok, então não é garantido. O compilador pode fazer isso? Sim! Aqui está uma função que calcula os números de Fibonacci por meio de uma função interna recursiva da cauda:
Não apenas a chamada
fibonacci_lr
final é eliminada, toda a função é incorporadafibonacci
, fornecendo apenas 12 instruções (e não umacall
vista):Se você comparar isso com um
while
loop equivalente , o compilador gera quase o mesmo assembly.Qual é o objetivo?
Você provavelmente não deve confiar em otimizações para eliminar chamadas finais, seja em Rust ou em C. É bom quando isso acontece, mas se você precisa ter certeza de que uma função é compilada em um loop apertado, da maneira mais certa, pelo menos para agora, é usar um loop.
fonte