Rust 递归函数计算阶乘导致堆栈溢出

Rust 递归函数计算阶乘导致堆栈溢出,rust,biginteger,factorial,bignum,Rust,Biginteger,Factorial,Bignum,我在Rust中尝试了递归阶乘算法。我使用此版本的编译器: rustc 1.12.0 (3191fbae9 2016-09-23) cargo 0.13.0-nightly (109cb7c 2016-08-19) 代码: extern crate num_bigint; extern crate num_traits; use num_bigint::{BigUint, ToBigUint}; use num_traits::One; fn factorial(num: u64) ->

我在Rust中尝试了递归阶乘算法。我使用此版本的编译器:

rustc 1.12.0 (3191fbae9 2016-09-23)
cargo 0.13.0-nightly (109cb7c 2016-08-19)
代码:

extern crate num_bigint;
extern crate num_traits;

use num_bigint::{BigUint, ToBigUint};
use num_traits::One;

fn factorial(num: u64) -> BigUint {
    let current: BigUint = num.to_biguint().unwrap();
    if num <= 1 {
        return One::one();
    }
    return current * factorial(num - 1);
}

fn main() {
    let num: u64 = 100000;
    println!("Factorial {}! = {}", num, factorial(num))
}

如何解决这个问题?为什么我在使用Rust时会看到这个错误?

Rust没有尾部调用消除,所以递归受到堆栈大小的限制。这可能是Rust未来的一个特性(您可以在网站上阅读更多关于它的内容),但与此同时,您必须要么不要递归太深,要么使用循环。

为什么? 这是一个堆栈溢出,每当没有剩余的堆栈内存时就会发生。例如,堆栈内存由

  • 局部变量
  • 函数参数
  • 返回值
递归使用了大量的堆栈内存,因为对于每个递归调用,所有局部变量、函数参数。。。必须在堆栈上分配


如何解决这个问题? 显而易见的解决方案是以非递归的方式编写算法(当您想在生产中使用算法时,应该这样做!)。但是您也可以增加堆栈大小。虽然无法修改主线程的堆栈大小,但可以创建新线程并设置特定的堆栈大小:

fn main() {
    let num: u64 = 100_000;
    // Size of one stack frame for `factorial()` was measured experimentally
    thread::Builder::new().stack_size(num as usize * 0xFF).spawn(move || {
        println!("Factorial {}! = {}", num, factorial(num));
    }).unwrap().join();
}
此代码有效,当通过
货物运行--release
(带优化!)执行时,只需几秒钟计算即可输出解决方案


测量堆栈帧大小 如果您想知道如何测量
factorial()
的堆栈帧大小(一次调用的内存需求):我在每个
factorial()调用上打印了函数参数
num
的地址:

fn factorial(num: u64) -> BigUint {
    println!("{:p}", &num);
    // ...
}
两个连续调用地址之间的差异(或多或少)是堆栈帧大小。在我的机器上,差异略小于
0xFF
(255),所以我只是将其用作大小

如果您想知道为什么堆栈帧大小没有变小:Rust编译器并没有针对这个指标进行真正的优化。通常这并不重要,所以优化器倾向于牺牲内存需求以获得更好的执行速度。我查看了程序集,在本例中,许多
BigUint
方法被内联。这意味着其他函数的局部变量也在使用堆栈空间

作为一种选择。。(我不推荐)

马特斯的回答在某种程度上是正确的。有一个名为
stacker
()的板条箱,可以人为地增加堆栈大小,以便在递归算法中使用。它通过分配一些堆内存来实现这一点

作为一句警告的话。。。这需要很长时间才能运行。。。但是,它运行,并且不会破坏堆栈。使用优化编译会降低速度,但速度仍然相当慢。正如马特所建议的那样,你可能会从循环中获得更好的表现。我想我会把这个扔出去的

extern crate num_bigint;
extern crate num_traits;
extern crate stacker;

use num_bigint::{BigUint, ToBigUint};
use num_traits::One;

fn factorial(num: u64) -> BigUint {
    // println!("Called with: {}", num);
    let current: BigUint = num.to_biguint().unwrap();
    if num <= 1 {
        // println!("Returning...");
        return One::one();
    }

    stacker::maybe_grow(1024 * 1024, 1024 * 1024, || {
        current * factorial(num - 1)
    })
}

fn main() {
    let num: u64 = 100000;
    println!("Factorial {}! = {}", num, factorial(num));
}
extern板条箱数量;
外部板条箱数量性状;
外部板条箱堆垛机;
使用num_bigint::{BigUint,ToBigUint};
使用num_traits::One;
fn阶乘(num:u64)->BigUint{
//println!(“调用时使用:{}”,num);
let current:BigUint=num.to_BigUint().unwrap();

if num回答得很好!但是阶乘bigint的循环还有另一个问题-非常慢。@mrLSD我不怀疑!但是递归,至少在Rust中,并没有真正提供比循环更快的速度。至少我没有听说过。@mrLSD虽然LLVM可以优化某些尾部调用,但问题中的函数没有尾部调用并不会进行任何优化,即使是在保证TCO的语言中也是如此注:与那只著名的猫非常相似,测量堆栈帧大小可能会增加它(在获取
num
的地址之前,它可能会留在寄存器中,
println!
也需要一些堆栈空间)。
可能会增长(1024*1024,1024*1024
将在每次调用时分配一个新的堆栈帧。1GB内存使用率。如果我将其更改为
可能会增加(32*1024,1024*1024
,在32K剩余之前,它不会分配新堆栈。现在它只使用20MB内存。不过,这种更改并不会真正改变速度。啊,是的,对不起-我本来在64k边界上有它,但在最后一次测试中增加了它。你说
我不建议使用
,但如果你可以的话,我认为这确实是一个很好的选择不要将递归算法重写为迭代算法。运行时堆栈溢出非常糟糕。当然,这个问题中的算法很容易编写迭代算法。