【问题标题】:When is tail recursion guaranteed in Rust?Rust 什么时候保证尾递归?
【发布时间】:2020-04-03 01:37:57
【问题描述】:

C语言

在C编程语言中,很容易有尾递归

int foo(...) {
    return 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),
    }
}

(这是一个最小的例子,真实的例子更复杂,但它抓住了主要思想)

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

是否保证 Rust 编译器会应用尾递归?

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

【问题讨论】:

  • 我相信你在谈论tail recursion,如果你说“tail”而不是“terminal”,你可能会更好地输入你的问题
  • “在 C 编程语言中,很容易保证终端递归”如果 C 做出任何这样的保证,我会感到惊讶。
  • 我认为您将尾递归与尾调用优化混合在一起。例如。您的 C 代码是尾递归的,但它可能会破坏堆栈,因为它不能保证 tail call optimization

标签: recursion rust tail-recursion


【解决方案1】:

Shepmaster's answer 解释说尾调用消除只是 Rust 中的一种优化,而不是保证。但是“从不保证”并不意味着“永远不会发生”。让我们看看编译器对一些真实的代码做了什么。

this函数中会发生这种情况吗?

截至目前,Compiler Explorer 上可用的最新 Rust 版本是 1.39,它确实没有消除了 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

注意这一行:call qword ptr [rip + example::read_all@GOTPCREL]。这就是(尾)递归调用。从它的存在可以看出,它并没有被淘汰。

Compare this to an equivalent function with an explicit loop:

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),
        }
    }
}

它没有要消除的尾调用,因此编译为一个只有一个call 的函数(到input.read 的计算地址)。

哦,好吧。也许 Rust 不如 C。或者是吗?

它发生在 C 中吗?

这是一个 C 中的尾递归函数,它执行非常相似的任务:

int read_all(FILE *input) {
    char buf[] = {0, 0};
    if (!fgets(buf, sizeof buf, input))
        return feof(input);
    return read_all(input);
}

这对于编译器来说应该非常容易消除。递归调用就在函数的底部,C 不必担心运行析构函数。但尽管如此,there's that recursive tail call,令人讨厌的是没有被淘汰:

        call    read_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 条指令(而不是call):

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

如果你compare this to an equivalent while loop,编译器会生成几乎相同的程序集。

有什么意义?

你可能不应该依赖优化来消除尾调用,无论是在 Rust 还是在 C 中。当它发生时很好,但如果你需要确保一个函数编译成一个紧密的循环,最可靠的方法,至少现在,是使用循环。

【讨论】:

    【解决方案2】:

    tail recursion(重用堆栈帧以对同一函数进行尾部调用)和 tail call optimization(重用堆栈帧以对 any 函数进行尾部调用)都不会被 Rust 保证,尽管优化器可能选择执行它们。

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

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

    另见:

    【讨论】:

      猜你喜欢
      • 2010-12-20
      • 1970-01-01
      • 2014-08-28
      • 2011-04-10
      • 2018-04-09
      • 2015-01-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多