web-dev-qa-db-ja.com

階乗を計算する再帰関数はスタックオーバーフローにつながります

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を使用しているときにこのエラーが表示されるのはなぜですか?

12
mrLSD

Rustには末尾呼び出しの除去がないため、再帰はスタックサイズによって制限されます。これは将来的にはRustの機能になる可能性があります(詳細は Rust FAQ で読むことができます)が、それまでは再発しないようにする必要があります。とても深いか、ループを使用します。

14
Matt

どうして?

これは、スタックメモリが残っていないときに発生するスタックオーバーフローです。たとえば、スタックメモリはによって使用されます

  • ローカル変数
  • 関数の引数
  • 戻り値

再帰呼び出しでは、すべてのローカル変数、関数の引数、...のメモリをスタックに割り当てる必要があるため、再帰は大量のスタックメモリを使用します。


それを修正する方法は?

明らかな解決策は、非再帰的な方法でアルゴリズムを作成することです(本番環境でアルゴリズムを使用する場合は、これを行う必要があります)。ただし、スタックサイズを増やすにすることもできます。メインスレッドのスタックサイズは変更できませんが、新しいスレッドを作成して特定のスタックサイズを設定できます。

_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();
}
_

このコードworksそして、_cargo run --release_(最適化あり!)を介して実行されると、わずか数秒の計算後に解を出力します。


スタックフレームサイズの測定

factorial()のスタックフレームサイズ(one呼び出しのメモリ要件)がどのように測定されたかを知りたい場合:私は各factorial()呼び出しでの関数引数numのアドレス:

_fn factorial(num: u64) -> BigUint {
    println!("{:p}", &num);
    // ...
}
_

2つの連続する呼び出しのアドレスの違いは、(多かれ少なかれ)スタックフレームサイズです。私のマシンでは、差は_0xFF_(255)よりわずかに小さかったので、サイズとして使用しました。

スタックフレームサイズが小さくない理由がわからない場合:Rustコンパイラはこのメトリックに対して実際には最適化されません。通常はそれほど重要ではないため、オプティマイザはこのメモリを犠牲にする傾向があります。実行速度を向上させるための要件。アセンブリを調べたところ、この場合、多くのBigUintメソッドがインライン化されました。これは、他の関数のローカル変数もスタックスペースを使用していることを意味します。

7

代わりに..(私はお勧めしません)

マットの答えはある程度真実です。再帰的アルゴリズムで使用するためにスタックサイズを人為的に増やすことができる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));
}

デバッグprintlnsをコメントアウトしました。必要に応じてコメントを解除できます。

7
Simon Whitehead