Recursion 什么时候在Rust中保证尾部递归? C语言

Recursion 什么时候在Rust中保证尾部递归? C语言,recursion,rust,tail-recursion,Recursion,Rust,Tail Recursion,在C编程语言中,很容易实现尾部递归: intfoo(…){ 返回foo(…); } 只是返回递归调用的返回值。当这种递归可能重复一千次甚至一百万次时,这一点尤为重要。它将在堆栈上使用大量的内存 生锈 现在,我有一个Rust函数,它可能会递归调用自己一百万次: fn read_all(input: &mut dyn std::io::Read) -> std::io::Result<()> { match input.read(&mut [0u8]) {

在C编程语言中,很容易实现尾部递归

intfoo(…){
返回foo(…);
}
只是返回递归调用的返回值。当这种递归可能重复一千次甚至一百万次时,这一点尤为重要。它将在堆栈上使用大量的内存

生锈 现在,我有一个Rust函数,它可能会递归调用自己一百万次:

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),
    }
}
fn read_all(输入:&mut dyn std::io::read)->std::io::Result{
匹配输入。读取(&mut[0u8]){
Ok(0)=>Ok(()),
正常(=>全部读取(输入),
Err(Err)=>Err(Err),
}
}
(这是一个最小的例子,真实的例子更复杂,但它抓住了主要思想)

这里,递归调用的返回值按原样返回,但是:

它是否保证Rust编译器将应用尾部递归?

例如,如果我们声明了一些需要销毁的变量,比如std::Vec,它会在递归调用之前销毁(允许尾部递归)还是在递归调用返回之后销毁(禁止尾部递归)?

既不销毁(对同一函数的尾部调用重用堆栈帧)也不销毁(重用堆栈框架以进行对任何函数的尾部调用)始终受到Rust的保证,尽管优化器可能会选择执行它们

如果我们声明了一些需要销毁的变量

我的理解是,这是症结之一,因为更改已销毁堆栈变量的位置会引起争议

另见:

解释了尾调用消除仅仅是一种优化,而不是一种保证,但是“从不保证”并不意味着“永远不会发生”。让我们来看看编译器对一些真实代码的操作。 它发生在这个函数中吗? 到目前为止,上可用的最新版本Rust为1.39,它并没有消除

read_all
中的尾部调用

示例::全部阅读:
推送r15
推动r14
推送rbx
副区长,32
mov r14,rdx
莫夫r15,rsi
mov-rbx,rdi
mov字节ptr[rsp+7],0
李尔迪[rsp+8]
lea rdx,[rsp+7]
mov ecx,1
呼叫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
日本脑炎3型2型
mov rdi,rbx
mov rsi,r15
mov-rdx,r14
调用qword ptr[rip+示例::读取_all@GOTPCREL]
jmp.LBB3_3
.LBB3_2:
mov字节ptr[rbx],3
.LBB3_3:
mov-rax,rbx
加上rsp,32
流行音乐
流行音乐r14
流行音乐r15
ret
mov-rbx,rax
李尔迪[rsp+8]
调用core::ptr::real\u drop\u就地
mov rdi,rbx
打电话给你放松一下_Resume@PLT
ud2
注意这行:
调用qwordptr[rip+example::read_all@GOTPCREL]
。这是(尾部)递归调用。从它的存在可以看出,它并没有被消除

:

这对于编译器来说应该非常容易消除。递归调用就在函数的底部,C不必担心运行析构函数。但是,令人烦恼的是,没有消除:

调用read\u all
事实证明,在C中也不能保证尾部调用优化。我在不同的优化级别下尝试了Clang和gcc,但我尝试的任何方法都无法将这个相当简单的递归函数变成循环

它曾经发生过吗? 好的,所以这不是保证。编译器能做到吗?是的!这是一个通过尾部递归内部函数计算斐波那契数的函数:

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)
}
不仅消除了尾部调用,整个
fibonacci_lr
函数被内联到
fibonacci
中,只产生12条指令(并且看不到
调用
):

示例::斐波那契:
推1
波普rdx
异或ecx,ecx
.LBB0_1:
mov-rax,rdx
测试rdi,rdi
乙脑LBB0_3
dec rdi
添加rcx,rax
mov-rdx,rcx
mov-rcx,rax
jmp.LBB0_1
.LBB0_3:
ret
如果需要,编译器将生成几乎相同的程序集

重点是什么?
您可能不应该依赖优化来消除尾部调用,无论是在Rust还是在C中。这种情况发生时很好,但如果您需要确保函数编译成一个紧密的循环,最可靠的方法是使用循环。

我相信您正在谈论的是,如果您说在C编程语言中,用“tail”代替“terminal”,终端递归很容易得到保证“如果C做了这样的保证,我会感到惊讶。我认为你是在混合尾部递归和尾部调用优化。例如,你的C代码是尾部递归的,但它可能会破坏堆栈,因为它不能保证
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)
}