Rust 递归函数计算阶乘导致堆栈溢出
我在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) ->
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边界上有它,但在最后一次测试中增加了它。你说我不建议使用,但如果你可以的话,我认为这确实是一个很好的选择不要将递归算法重写为迭代算法。运行时堆栈溢出非常糟糕。当然,这个问题中的算法很容易编写迭代算法。