Python中的递归,memoization和可变默认参数

问题描述 投票:4回答:1

“Base”意思是不使用lru_cache。所有这些都“足够快” - 我不是在寻找最快的算法 - 但时间让我感到惊讶,所以我希望我能学到一些关于Python“工作原理”的东西。

简单循环(/尾递归):

def fibonacci(n):
    a, b = 0, 1
    if n in (a, b): return n
    for _ in range(n - 1):
        a, b = b, a + b
    return b

简单记忆:

def fibonacci(n, memo={0:0, 1:1}):
    if len(memo) <= n:
        memo[n] = fibonacci(n - 1) + fibonacci(n - 2)
    return memo[n]

使用发电机:

def fib_seq():
    a, b = 0, 1
    yield a
    yield b
    while True:
        a, b = b, a + b
        yield b

def fibonacci(n):
    return next(x for (i, x) in enumerate(fib_seq()) if i == n)

我期望第一个,简单易行,是最快的。不是。尽管有递归和大量函数调用,但第二个是迄今为止最快的。第三个很酷,并使用“现代”功能,但更慢,这是令人失望的。 (我很想把发电机想象成某种方式来替代记忆 - 因为它们记住了它们的状态 - 而且由于它们是用C实现的,我希望它们能更快。)

典型结果:

loop: about 140 μs
memo: about 430 ns
genr: about 250 μs

那么,任何人都能解释一下,为什么memoization比简单循环快一个数量级?

编辑:

现在清楚,我(像我之前的很多人)只是偶然发现了Python的可变默认参数。此行为解释了执行速度的实际和明显增益。

python performance optimization fibonacci memoization
1个回答
1
投票

你所看到的是记忆的全部要点。第一次调用该函数时,memo缓存为空,必须递归。但是下次使用相同或更低的参数调用它时,答案已经在缓存中,因此它会立即返回。如果您执行数千个呼叫,则您将所有其他呼叫的第一个呼叫时间分摊。这就是使memoization成为如此有用的优化的原因,你只需要第一次支付成本。

如果要查看缓存是新鲜的并且必须执行所有递归所需的时间,可以将初始缓存作为基准调用中的显式参数传递:

fibonacci(100, {0:0, 1:1})
© www.soinside.com 2019 - 2024. All rights reserved.