After watching an MIT lecture on dynamic programming, I felt like practicing a bit with Fibonacci. I first wrote the naive recursive implementation and then added memoization. Here is the memoized version:
package main
import (
"fmt"
)
func fib_memoized(n int, memo map[int]int64) int64 {
memoized, ok := memo[n]
if ok {
return memoized
}
if n < 2 {
return int64(n)
}
f := fib_memoized(n-2, memo) + fib_memoized(n-1, memo)
memo[n] = f
return f
}
func main() {
memo := make(map[int]int64)
for i := 0; i < 10000; i++ {
fmt.Printf("fib(%d) = %d\n", i, fib_memoized(i, memo))
}
}
I then proceeded to write a non-recursive version of the program:
package main
import (
"fmt"
)
func fib(n int) int64 {
var f1 int64 = 1
var f2 int64 = 0
for i := 0; i < n; i++ {
f1, f2 = f2, f1+f2
}
return f2
}
func main() {
for i := 0; i < 10000; i++ {
fmt.Printf("fib(%d) = %d\n", i, fib(i))
}
}
What has puzzled me is the fact that the memoized version seems to perform at least as well as the non-recursive one and, sometimes, even outperform it. Naturally, I was expecting memoization to bring great improvements in comparison to the naive recursive implementation, but I just haven't been able to figure out why/how the memoized version could be on par and even surpass its non-recursive counterpart.
I did try looking into the assembly output (obtained with go tool compile -S) of both versions, but to no avail. I still see the CALL instructions in the memoized version and, to my mind, that should incur enough overhead to justify it being at least slightly outperformed by the non-recursive version.
Could anyone more knowledgeable help me understand what's going on?
P.S. I'm aware of integer overflow; I used 10000 just to increase the load.
Thank you.