【发布时间】:2021-09-02 12:15:57
【问题描述】:
在观看了 MIT 关于动态编程的讲座后,我想练习一下斐波那契。我首先编写了朴素的递归实现,然后添加了 memoization。这是记忆的版本:
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))
}
}
然后我开始编写程序的非递归版本:
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))
}
}
让我感到困惑的是,记忆版本的性能似乎至少与非递归版本一样好,有时甚至胜过它。自然地,我期望记忆化与朴素的递归实现相比会带来很大的改进,但我只是无法弄清楚为什么/如何记忆化的版本可以达到甚至超过其非递归版本。
我确实尝试查看两个版本的程序集输出(使用go tool compile -S 获得),但无济于事。我仍然在记忆版本中看到CALL 指令,并且在我看来,这应该会产生足够的开销来证明它至少比非递归版本略胜一筹。
谁能帮助我了解发生了什么?
附:我知道整数溢出;我用10000只是为了增加负载。
谢谢。
【问题讨论】:
-
您似乎指的是记忆递归、朴素递归和非递归实现,但您只显示了两个。您正在运行哪些,哪些预计会最快?你能用 Go 基准测试重写这个问题吗?我希望非递归实现甚至比记忆递归实现更快,因为 a)Go 没有很好的尾调用优化,b)你的记忆实现不是尾调用。
-
我没有包含幼稚版本的原因是因为我认为它不会有利于讨论,因为我的问题实际上是关于记忆递归与非递归(或迭代)版本。很抱歉没有使用 Go 基准测试。无论如何,您似乎已经为此添加了一个响应,我将对其进行研究。谢谢。
标签: algorithm performance go recursion