我注意到一个次要重构的一个奇怪的性能损失,它通过在递归函数内调用内置max
来替换一个循环。
这是我可以制作的最简单的复制品:
import time
def f1(n):
if n <= 1:
return 1
best = 0
for k in (1, 2):
current = f1(n-k)*n
if current > best:
best = current
return best
def f2(n):
if n <= 1:
return 1
return max(f2(n-k)*n for k in (1, 2))
t = time.perf_counter()
result1 = f1(30)
print('loop:', time.perf_counter() - t) # 0.45 sec
t = time.perf_counter()
result2 = f2(30)
print('max:', time.perf_counter() - t) # 1.02 sec
assert result1 == result2
f1
和f2
都使用标准递归计算阶乘,但添加了不必要的最大化(这样我就可以在递归中使用max
,同时仍然保持递归简单) :
# pseudocode
factorial(0) = 1
factorial(1) = 1
factorial(n) = max(factorial(n-1)*n, factorial(n-2)*n)
它是在没有记忆的情况下实现的,所以有一个指数的呼叫。
使用max(iterable)
的实现比使用循环的实现慢两倍。
奇怪的是,(编辑:没关系,请参阅@TimPeters答案)。另外,如果我使用max
与循环的直接比较没有证明效果max(a, b)
代替max(iterable)
,则性能不匹配会消失。
答案 0 :(得分:7)
将此作为“答案”发布,因为评论中无法使用有用的格式:
$ python -m timeit "max(1, 2)" # straight
10000000 loops, best of 3: 0.148 usec per loop
$ python -m timeit "max([i for i in (1, 2)])" # list comp
1000000 loops, best of 3: 0.328 usec per loop
$ python -m timeit "max(i for i in (1, 2))" # genexp
1000000 loops, best of 3: 0.402 usec per loop
这表明递归是一个红鲱鱼。通常情况下,正如这些结果所示,genexp比listcomp慢,后者反过来比使用两者都慢。由于你的代码比只 a max更多,时间差异并不是那么极端 - 但是因为它只是小而不仅仅是最大值,所以最大速度部分是非常重要的。
答案 1 :(得分:4)
由于您正在为其提供生成器表达式,因此对max
函数非常不公平。
对于f2
的每次调用,都需要为n
创建一个新的闭包,需要创建一个新函数(这就是生成器表达式和Python 3中的列表表达式,我相信,已实现;请参阅'The Details' of PEP 289),其中包含代表gen-exp的代码对象。然后,这个迭代调用其他函数的函数再次被调用。
有问题的字节代码的一小部分:
14 LOAD_CLOSURE 0 (n)
16 BUILD_TUPLE 1
18 LOAD_CONST 2 (<code object <genexpr> at 0x7f1b667e1f60, file "", line 16>)
20 LOAD_CONST 3 ('f2.<locals>.<genexpr>')
22 MAKE_FUNCTION 8
24 LOAD_CONST 5 ((1, 2))
26 GET_ITER
28 CALL_FUNCTION 1
你当然没有在f1
的案例中看到任何这样的指示,因为它只是在做电话。
当您再次调用max
函数f2
时,重要次,正如您在递归计算30
的阶乘时所做的那样,开销只是堆积。
功能事物的列表理解版本几乎遭受同样的问题。它有点快,因为列表推导比生成器表达式更快。</ p>
如果我使用
max(a, b)
代替max(iterable)
,性能不匹配就会消失。
在这种情况下,没有为每个调用创建任何函数,因此您没有看到这些开销堆积。你只是在这里提供论据。