递归函数计算阶乘导致栈溢出
Recursive function calculating factorials leads to stack overflow
我在 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))
}
我收到这个错误:
$ cargo run
thread 'main' has overflowed its stack
fatal runtime error: stack overflow
error: Process didn't exit successfully
如何解决?为什么我在使用 Rust 时会看到此错误?
Rust 没有尾调用消除,因此您的递归受堆栈大小的限制。它可能是 Rust 将来的一个特性(你可以在 Rust FAQ 阅读更多关于它的信息),但与此同时你将不得不要么不那么深入地递归,要么使用循环。
为什么?
这是堆栈溢出,只要没有剩余的堆栈内存就会发生。例如,堆栈内存被
使用
- 局部变量
- 函数参数
- return 值
递归使用大量堆栈内存,因为对于每个递归调用,所有局部变量、函数参数...的内存都必须在堆栈上分配。
How to fix that?
显而易见的解决方案是以非递归方式编写您的算法(当您想在生产中使用该算法时,您应该这样做!)。但您也可以 增加筹码量 。虽然主线程的堆栈大小无法修改,但您可以创建一个新线程并设置特定的堆栈大小:
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();
}
此代码 有效 并且,当通过 cargo run --release
执行时(经过优化!),只需几秒钟的计算即可输出解决方案。
正在测量堆栈帧大小
如果您想知道如何测量 factorial()
的堆栈帧大小(one 调用的内存要求):我打印了函数参数的地址num
每次 factorial()
调用:
fn factorial(num: u64) -> BigUint {
println!("{:p}", &num);
// ...
}
两个连续调用地址之间的差异(或多或少)是堆栈帧的大小。在我的机器上,差异略小于 0xFF
(255),所以我只是将其用作大小。
如果你想知道为什么堆栈帧大小没有变小:Rust 编译器并没有真正针对这个指标进行优化。通常它真的不重要,所以优化器倾向于牺牲这个内存需求以获得更好的执行速度。我查看了程序集,在本例中内联了许多 BigUint
方法。这意味着其他函数的局部变量也在使用堆栈space!
作为替代..(我不推荐)
马茨的回答在一定程度上是正确的。有一个名为 stacker
(here) 的箱子可以人为地增加堆栈大小以用于递归算法。它通过分配一些堆内存溢出来做到这一点。
作为一个警告...这需要 很长时间才能 运行...但是,它 运行s,它不会炸毁堆栈。通过优化进行编译会降低它的性能,但它仍然很慢。正如马特建议的那样,您可能会从循环中获得更好的性能。我想无论如何我都会把它扔出去。
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));
}
我已经注释掉了调试 println
s.. 如果您愿意,可以取消注释。
我在 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))
}
我收到这个错误:
$ cargo run
thread 'main' has overflowed its stack
fatal runtime error: stack overflow
error: Process didn't exit successfully
如何解决?为什么我在使用 Rust 时会看到此错误?
Rust 没有尾调用消除,因此您的递归受堆栈大小的限制。它可能是 Rust 将来的一个特性(你可以在 Rust FAQ 阅读更多关于它的信息),但与此同时你将不得不要么不那么深入地递归,要么使用循环。
为什么?
这是堆栈溢出,只要没有剩余的堆栈内存就会发生。例如,堆栈内存被
使用- 局部变量
- 函数参数
- return 值
递归使用大量堆栈内存,因为对于每个递归调用,所有局部变量、函数参数...的内存都必须在堆栈上分配。
How to fix that?
显而易见的解决方案是以非递归方式编写您的算法(当您想在生产中使用该算法时,您应该这样做!)。但您也可以 增加筹码量 。虽然主线程的堆栈大小无法修改,但您可以创建一个新线程并设置特定的堆栈大小:
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();
}
此代码 有效 并且,当通过 cargo run --release
执行时(经过优化!),只需几秒钟的计算即可输出解决方案。
正在测量堆栈帧大小
如果您想知道如何测量 factorial()
的堆栈帧大小(one 调用的内存要求):我打印了函数参数的地址num
每次 factorial()
调用:
fn factorial(num: u64) -> BigUint {
println!("{:p}", &num);
// ...
}
两个连续调用地址之间的差异(或多或少)是堆栈帧的大小。在我的机器上,差异略小于 0xFF
(255),所以我只是将其用作大小。
如果你想知道为什么堆栈帧大小没有变小:Rust 编译器并没有真正针对这个指标进行优化。通常它真的不重要,所以优化器倾向于牺牲这个内存需求以获得更好的执行速度。我查看了程序集,在本例中内联了许多 BigUint
方法。这意味着其他函数的局部变量也在使用堆栈space!
作为替代..(我不推荐)
马茨的回答在一定程度上是正确的。有一个名为 stacker
(here) 的箱子可以人为地增加堆栈大小以用于递归算法。它通过分配一些堆内存溢出来做到这一点。
作为一个警告...这需要 很长时间才能 运行...但是,它 运行s,它不会炸毁堆栈。通过优化进行编译会降低它的性能,但它仍然很慢。正如马特建议的那样,您可能会从循环中获得更好的性能。我想无论如何我都会把它扔出去。
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));
}
我已经注释掉了调试 println
s.. 如果您愿意,可以取消注释。