Рекурсивная функция вычисления факториалов приводит к переполнению стека

я попробовал рекурсивный факториальный алгоритм в 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

как исправить? И почему я вижу эту ошибку при использовании ржавчина?

3 ответов


Rust не имеет исключения хвостового вызова, поэтому ваша рекурсия ограничена размером стека. Это может быть функция для ржавчины в будущем (вы можете прочитать больше об этом в часто задаваемые вопросы Руст), но в то же время вам придется либо не повторить столь глубок и использовать циклы.


просто в качестве альтернативы.. (Не рекомендую)

ответ Matts верен до некоторой степени. Существует ящик под названием stacker (здесь), который может искусственно увеличить размер стека для использования в рекурсивных алгоритмах. Он делает это, выделяя некоторую память кучи для переполнения.

в качестве предупреждения... это занимает очень много времени ... но он работает, и он не взрывает стек. Компиляция с оптимизациями приносит его вниз, но это все еще довольно медленно. Вы, вероятно, получите лучший perf от цикла, как предлагает Мэтт. Я думал, что все равно выброшу это.

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

Я прокомментировал debug printlns.. вы можете раскомментировать их, если хотите.


почему?

это переполнение стека, которое происходит всякий раз, когда нет стековой памяти. Например, память стека используется

  • локальные переменные
  • аргументов функции
  • возвращаемые значения

рекурсия использует много памяти стека, потому что для каждого рекурсивного вызова память для всех локальных переменных, аргументов функции,... должен быть выделен в стеке.


как исправить?

очевидное решение-написать свой алгоритм нерекурсивным способом (вы должны сделать это, когда хотите использовать алгоритм в производстве!). Но вы также можете просто увеличить размер стека. В то время как размер стека основного потока не могу быть измененным, вы можете создать новый поток и установить определенный размер стека:

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() было измерено: я напечатал адрес аргумента функции num в каждом factorial() звоните:

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

разница между двумя последовательными адресами вызовов является (более или менее) размером кадра стека. На мой машина, разница была чуть меньше 0xFF (255), поэтому я просто использовал это как размер.

если вам интересно, почему размер кадра стека не меньше: компилятор Rust на самом деле не оптимизируется для этой метрики. Обычно это действительно не важно, поэтому оптимизаторы склонны жертвовать этим требованием памяти для лучшей скорости выполнения. Я взглянул на собрание, и в этом случае многие методы. Это означает, что локальные переменные других функций использование пространства стека!