为什么all()慢于使用for-else&突破?

时间:2016-04-05 14:55:41

标签: python performance primes

我一直在使用来自项目Euler的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的速度以保持问题清晰。对不起男士。

3 个答案:

答案 0 :(得分:1)

我可能错了,但我认为每次它在生成器表达式中计算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

答案 1 :(得分:1)

这是几个问题的组合:

  1. 调用内置函数并加载和执行生成器代码对象的设置成本很低,因此对于要测试的少量素数,设置成本会降低每个测试成本
  2. 生成器表达式建立内部范围;未被迭代的变量需要经过正常LEGB lookup成本,因此all生成器表达式中的每次迭代都需要查找test以确保它没有“{1}}成本。 t改变了,它是通过dict查找(其中局部变量查找是固定大小数组中的廉价查找)来实现的。
  3. 生成器有很少的开销,特别是在跳入和跳出Python字节代码时(all在CPython的C层实现)
  4. 您可以采取哪些措施来尽量减少差异或消除差异:

    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:]))(通过预先查询test.__mod__而不是每次循环一次,也可以达到#2)
    4. 如果输入足够大,#3可以有时赢得原始代码,至少在Python 3.5上(我在ipython中进行微基准测试),取决于许多因素。它并不总是赢,因为BINARY_MODULO的字节码解释器中有一些优化适用于可以直接跳到int.__mod__代码旁路的CPU寄存器的值,但它通常会执行非常相似。

答案 2 :(得分:0)

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

但是,可以使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%。

在jupyter notebook w python 3.4.4中使用%%timit在一个古老的macbook air上:

%%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