Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is my recursive Fibonacci implementation so slow compared to an iterative one?

I have created the following simple Fibonacci implementations:

#![feature(test)]
extern crate test;

pub fn fibonacci_recursive(n: u32) -> u32 {
    match n {
        0 => 0,
        1 => 1,
        _ => fibonacci_recursive(n - 1) + fibonacci_recursive(n - 2)
    }
}

pub fn fibonacci_imperative(n: u32) -> u32 {
    match n {
        0 => 0,
        1 => 1,
        _ => {
            let mut penultimate;
            let mut last = 1;
            let mut fib = 0;
            for _ in 0..n {
                penultimate = last;
                last = fib;
                fib = penultimate + last;
            }
            fib
        }
    }
}

I created them to try out cargo bench, so I wrote the following benchmarks:

#[cfg(test)]
mod tests {
    use super::*;
    use test::Bencher;

    #[bench]
    fn bench_fibonacci_recursive(b: &mut Bencher) {
        b.iter(|| {
            let n = test::black_box(20);
            fibonacci_recursive(n)
        });
    }

    #[bench]
    fn bench_fibonacci_imperative(b: &mut Bencher) {
        b.iter(|| {
            let n = test::black_box(20);
            fibonacci_imperative(n)
        });
    }
}

I know that a recursive implementation is generally slower than an imperative one, especially since Rust doesn't support tail recursion optimization (which this implementation couldn't use anyways). But I was not expecting the following difference of nearly 2'000 times:

running 2 tests
test tests::bench_fibonacci_imperative ... bench:          15 ns/iter (+/- 3)
test tests::bench_fibonacci_recursive  ... bench:      28,435 ns/iter (+/- 1,114)

I ran it both on Windows and Ubuntu with the newest Rust nightly compiler (rustc 1.25.0-nightly) and obtained similar results.

Is this speed difference normal? Did I write something "wrong"? Or are my benchmarks flawed?

like image 642
Jan Hohenheim Avatar asked Dec 13 '22 18:12

Jan Hohenheim


2 Answers

As said by Shepmaster, you should use accumulators to keep the previously calculated fib(n - 1) and fib(n - 2) otherwise you keep calculating the same values:

pub fn fibonacci_recursive(n: u32) -> u32 {
    fn inner(n: u32, penultimate: u32, last: u32) -> u32 {
        match n {
            0 => penultimate,
            1 => last,
            _ => inner(n - 1, last, penultimate + last),
        }
    }
    inner(n, 0, 1)
}

fn main() {
    assert_eq!(fibonacci_recursive(0), 0);
    assert_eq!(fibonacci_recursive(1), 1);
    assert_eq!(fibonacci_recursive(2), 1);
    assert_eq!(fibonacci_recursive(20), 6765);
}

last is equivalent to fib(n - 1).
penultimate is equivalent to fib(n - 2).

like image 177
Boiethios Avatar answered Dec 19 '22 12:12

Boiethios


The algorithmic complexity between the two implementations differs:

  • your iterative implementation uses an accumulator: O(N),
  • your recursive implementation doesn't: O(1.6N).

Since 20 (N) << 12089 (1.6N), it's pretty normal to have a large difference.

See this answer for an exact computation of the complexity in the naive implementation case.

Note: the method you use for the iterative case is called dynamic programming.

like image 30
Matthieu M. Avatar answered Dec 19 '22 10:12

Matthieu M.