为什么max(iterable)比等效循环执行得慢得多?

时间:2017-06-17 17:36:51

标签: python performance python-3.x python-internals

我注意到一个次要重构的一个奇怪的性能损失,它通过在递归函数内调用内置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

f1f2都使用标准递归计算阶乘,但添加了不必要的最大化(这样我就可以在递归中使用max,同时仍然保持递归简单) :

# pseudocode
factorial(0) = 1
factorial(1) = 1
factorial(n) = max(factorial(n-1)*n, factorial(n-2)*n)

它是在没有记忆的情况下实现的,所以有一个指数的呼叫。

使用max(iterable)的实现比使用循环的实现慢两倍。

奇怪的是,max与循环的直接比较没有证明效果(编辑:没关系,请参阅@TimPeters答案)。另外,如果我使用max(a, b)代替max(iterable),则性能不匹配会消失。

2 个答案:

答案 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),性能不匹配就会消失。

在这种情况下,没有为每个调用创建任何函数,因此您没有看到这些开销堆积。你只是在这里提供论据。