【问题标题】:Why is all() slower than using for-else & break?为什么 all() 比使用 for-else & break 慢?
【发布时间】:2016-04-05 14:55:40
【问题描述】:

我一直在玩欧拉项目中的problem 7,我注意到我的两个主要查找方法非常相似,但运行速度却大不相同。

#!/usr/bin/env python3

import timeit

def lazySieve (num_primes):
    if num_primes == 0: return []
    primes = [2]
    test = 3
    while len(primes) < num_primes:
        sqrt_test = sqrt(test)
        if all(test % p != 0 for p in primes[1:]):  # I figured this would be faster
            primes.append(test)
        test += 2
    return primes

def betterLazySieve (num_primes):
    if num_primes == 0: return []
    primes = [2]
    test = 3
    while len(primes) < num_primes:
        for p in primes[1:]: # and this would be slower
            if test % p == 0: break
        else:
            primes.append(test)
        test += 2
    return primes

if __name__ == "__main__":

    ls_time  = timeit.repeat("lazySieve(10001)",
                             setup="from __main__ import lazySieve",
                             repeat=10,
                             number=1)
    bls_time = timeit.repeat("betterLazySieve(10001)",
                             setup="from __main__ import betterLazySieve",
                             repeat=10,
                             number=1)

    print("lazySieve runtime:       {}".format(min(ls_time)))
    print("betterLazySieve runtime: {}".format(min(bls_time)))

运行时输出如下:

lazySieve runtime:       4.931611961917952
betterLazySieve runtime: 3.7906006319681183

this 问题不同,我不只是想要任何/全部的返回值。

来自all() 的返回是否如此缓慢以至于如果覆盖它在所有但大多数情况下的使用? for-else 的中断是否比短路的 all() 更快?

你怎么看?

编辑:Reblochon Masque建议的平方根循环终止检查中添加

更新:ShadowRanger 的answer 是正确的。

改变后

all(test % p != 0 for p in primes[1:])

all(map(test.__mod__, primes[1:]))

我记录了以下运行时间的减少:

lazySieve runtime:       3.5917471940629184
betterLazySieve runtime: 3.7998314710566774

编辑:移除了 Reblochon 的加速以保持问题清晰。对不起,伙计。

【问题讨论】:

  • 我认为在一台机器上对小样本进行性能测试并没有多大意义
  • The Sieve of Eratosthenes 可能会更快。
  • @FredLarson 不是我要问的,而是回复您的评论。我之前已经实现了它,并且从我的测试来看,如果你能正确猜出筛子大小的一个好的上限,它只会更快。否则,这更像是一场赌博。
  • @cricket_007 我已经多次运行这个测试并且得到了稳定和相似的答案。在 ~0.01 秒内。
  • 加一个很好用 for-else :-)

标签: python performance primes


【解决方案1】:

这是几个问题的组合:

  1. 调用内置函数以及加载和执行生成器代码对象的设置成本是半成本的,因此对于少量要测试的素数,设置成本会淹没每次测试的成本
  2. 生成器表达式建立一个内部范围;未迭代的变量需要经过正常的LEGB lookup 成本,因此all 的生成器表达式中的每次迭代都需要查找test 以确保它没有改变,并且它通过@987654324 这样做@lookup(其中局部变量查找是固定大小数组中的廉价查找)
  3. 生成器的开销很小,尤其是在跳入和跳出 Python 字节码时(all 在 CPython 的 C 层实现)

您可以采取哪些措施来最小化或消除差异:

  1. 在更大的迭代器上运行测试(以尽量减少设置成本的影响)
  2. test 显式拉入生成器的本地范围,例如作为一个愚蠢的黑客all(test % p != 0 for test in (test,) for p in primes[1:])
  3. 使用带有 C 内置函数的 map 从进程中删除所有字节码执行,例如all(map(test.__mod__, primes[1:]))(这也恰好实现了 #2,通过预先查找 test.__mod__ 一次,而不是每个循环一次)

如果输入足够大,#3 可以有时胜过您的原始代码,至少在 Python 3.5(我在 ipython 中进行微基准测试)上,这取决于许多因素。它并不总能获胜,因为在 BINARY_MODULO 的字节码解释器中有一些优化,这些值可以放入 CPU 寄存器中,显式直接跳过 int.__mod__ 代码绕过,但它通常执行非常相似。

【讨论】:

  • 是的,它做到了。使用for test in (test,) hack 时,我没有注意到性能的变化。然而,map(test.__mod__, primes[1:]) 成功了。
【解决方案2】:

我可能是错的,但我认为每次它在生成器表达式中计算 test % p != 0 时,它都是在一个新的堆栈帧中这样做的,所以调用一个函数会有类似的开销。您可以在回溯中看到堆栈帧的证据,例如:

>>> all(n/n for n in [0])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in <genexpr>
ZeroDivisionError: integer division or modulo by zero

【讨论】:

  • 我没有想到这一点,但答案是有道理的。我将进行更多调查,看看是否发生了这种情况。
  • 每次都不是新的堆栈帧,没有支付 Python 级别的函数调用开销。 一个新的堆栈帧(单个​​帧,所有生成器表达式都涉及),但它不是每次迭代的新堆栈帧(描述性地,当每个值为 @ 时,堆栈帧正在被保存和恢复987654323@ed,但保存/恢复的成本远低于正常调用 Python 函数的成本,因为生成器协议经过大量优化)。
  • @ShadowRanger 知道这很酷。尽管如此,帧之间的切换不应该有一些开销吗?
  • 有,但这只是等式的一部分。加载代码对象、在其上创建闭包并调用闭包(创建初始帧)、然后加载 all(LEGB 中的 B 是一个杀手)并调用它还有一次性成本,所有这些都增加了设置开销,以至于它淹没了循环中的实际工作以进行小型循环,并在每次迭代时执行非局部变量的查找(相当于 Python 级别 dict 查找与用于访问本地的简单 C 级固定数组索引查找),这增加了“每个循环”的工作量。
【解决方案3】:

这是一个关于令人费解的结果的有趣问题,很遗憾我没有明确的答案......也许是因为样本量,或者这个计算的细节?但和你一样,我觉得这很令人惊讶。

但是,可以使lazysievebetterlazysieve 更快:

def lazySieve (num_primes):
    if num_primes == 0: 
        return []
    primes = [2]
    test = 3
    while len(primes) < num_primes:
        if all(test % p for p in primes[1:] if p <= sqr_test):
            primes.append(test)
        test += 2
        sqr_test = test ** 0.5
    return primes

它在您的版本中运行大约 65% 的时间,并且比我的系统上的 betterlazysieve 快​​大约 15%。

在老式 macbook air 上使用带有 python 3.4.4 的 jupyter notebook 中的 %%timit

%%timeit 
lazySieve(10001)
# 1 loop, best of 3: 8.19 s per loop

%%timeit
betterLazySieve(10001)
# 1 loop, best of 3: 10.2 s per loop

【讨论】:

  • 感谢您的意见!有趣的是,在我添加 sqrt_test 之后,它确实运行得更快,但仍然没有更快更好的 LazySieve。速度提高 13% 至 4.23 秒。
  • 对不起,堆栈上的第一个问题,仍在弄清楚这一切是如何工作的!它记录了赞成票,但由于我的级别太低而无法显示。很抱歉引用,找不到推荐的方法,所以我只是猜测。
  • 那不用担心,很高兴我能帮上忙。
猜你喜欢
  • 1970-01-01
  • 2020-11-22
  • 1970-01-01
  • 2019-12-08
  • 1970-01-01
  • 2017-11-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多