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?
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)
.
The algorithmic complexity between the two implementations differs:
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With