【问题标题】:Why does max(iterable) perform much slower than an equivalent loop?为什么 max(iterable) 的执行速度比等效循环慢得多?
【发布时间】:2017-06-17 17:36:50
【问题描述】:

我注意到一个小的重构对性能造成了奇怪的影响,该重构用递归函数内的内置 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),性能不匹配就会消失。

【问题讨论】:

  • 这种性能差距与您传递给 max: max(f2(n-k)*n for k in (1, 2) 的生成器表达式有关。生成器在空间优化方面表现良好,但在速度方面表现不佳。发电机需要暂停并继续,直到它们完全耗尽。这种暂停/恢复对性能造成很大影响。
  • @MosesKoledoye 问题是,这可能是不同形式的机器。一般来说,生成器往往很慢,它们必须暂停然后恢复工作。
  • @direprobs 不管怎样,max 版本仍然较低,即使没有 gen。 exp.
  • @MosesKoledoye 有趣。我还没有测试过,我对生成器进行了一些测试,与for 循环进行了比较。

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


【解决方案1】:

将此作为“答案”发布,因为在 cmets 中无法使用有用的格式:

$ 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 慢,而 listcomp 反过来又比不使用两者都慢。由于您的代码所做的不仅仅是 一个最大值,因此时间差异并没有那么极端 - 但由于它所做的 little 不仅仅是一个最大值,因此最大值的速度部分仍然非常重要。

【讨论】:

  • 我使用了 list comps 并用list.sort 排序,然后索引了最大数量L[-1],我在时间上得到了非常轻微的改进。虽然,这增加了每次条件 n &lt;= 1 为假时对列表进行排序的开销,但与使用 max 的生成器表达式相比,我能够获得(非常)轻微的改进。
  • @TimPeters 啊,我没有看到这个(所以最终使用递归再现)的原因是我的循环实现得很差 - 我使用了比 int 慢 3 倍的 float 数字。 python -m timeit "best=-float('inf')" "for i in (1, 2):" " if i &gt; best:" " best=i" 显示的时间几乎与您的 genexp 版本相同,但当然将 best 设置为第一个元素(因此将其保留为 int)使其超级快(就像您的 straight 版本一样)。
【解决方案2】:

这对于 max 函数来说真的很不公平,因为你正在输入生成器表达式。

对于f2 的每次调用,都需要为n 创建一个新的闭包,还需要创建一个新函数(这就是生成器表达式和我相信自Python 3 以来的列表表达式的实现方式;参见@ 987654321@) 封装了代表 gen-exp 的代码对象。然后这个迭代调用其他函数的函数再次被调用。

相关字节码的小sn-p:

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 的阶乘时所做的那样,开销只是 >堆积

函数事物的列表理解版本几乎会遇到同样的问题。它有点快,因为列表推导比生成器表达式更快。

如果我使用max(a, b) 而不是max(iterable),性能不匹配就会消失。

确实,在这种情况下,不会为每个调用创建函数,因此您不会看到开销堆积。您只是在此处提供参数。

【讨论】:

  • 这是有道理的;我知道 gen exp 不是免费的,我只是没想到成本会很高(在实际程序中使用 max(iterable) 时,我看到递归函数的运行时间增加了约 20%——不是在这个玩具示例中)。我想知道是否有任何方法可以在没有这种开销的情况下实现类似生成器表达式的构造(也许以其他一些缺点为代价)?
猜你喜欢
  • 2019-12-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-09-29
  • 2023-04-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多