Python 为什么all()比用于else&;慢;打破
我一直在愚弄ProjectEuler,我注意到我的两个主要查找方法非常相似,但运行速度非常不同Python 为什么all()比用于else&;慢;打破,python,performance,primes,Python,Performance,Primes,我一直在愚弄ProjectEuler,我注意到我的两个主要查找方法非常相似,但运行速度非常不同 #!/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)
#!/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)))
和问题不同的是,我不只是想要任何/全部的返回值
从all()
返回的速度是否太慢,以至于在大多数情况下都会覆盖它的使用?else的断开是否比短路的all()快
你觉得怎么样
编辑:在建议的平方根循环终止检查中添加
更新:暗影游侠的描述是正确的
更换后
all(对于素数[1:]中的p,测试%p!=0)
到
all(map(test.\u mod\u,primes[1:])
我在运行时记录了以下减少:
lazySieve runtime: 3.5917471940629184
betterLazySieve runtime: 3.7998314710566774
编辑:删除了Reblochon的加速,以保持问题清晰。抱歉,伙计。我可能错了,但我认为每次它计算测试%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
>全部(n/n表示[0]中的n)
回溯(最近一次呼叫最后一次):
文件“”,第1行,在
文件“”,第1行,在
ZeroDivisionError:整数除法或模零除法
这是一个关于一个令人费解的结果的有趣问题,不幸的是,我没有一个明确的答案。。。也许是因为样本量,或者是这个计算的细节?但和你一样,我也感到惊讶
但是,可以使lazysieve
比betterlazysieve
更快:
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
这是几个问题的结合:
调用内置函数以及加载和执行generator代码对象的设置成本是半昂贵的,因此对于少量要测试的素数,设置成本会超过每次测试的成本
生成器表达式建立内部范围;未被迭代的变量需要经过正常的代价,因此all
的生成器表达式中的每次迭代都需要查找test
,以确保它没有发生更改,并且它通过dict
查找(局部变量查找是固定大小数组中的廉价查找)
生成器的开销很小,特别是在跳入和跳出Python字节码时(all
在CPython的C层实现)
您可以采取哪些措施来最小化差异或消除差异:
在较大的iterables上运行测试(以最小化设置成本的影响)
显式地将test
拉入生成器的本地范围,例如,作为愚蠢的黑客all(test%p!=0表示test in(test),表示p in primes[1:])
使用带有C内置项的map
,例如all(map(test.\uu mod\uuuuuuuuu,primes[1:])
(通过查找test.\uu mod\uuuuuuuuu
一次,而不是每个循环一次,恰好实现了#2)
有了足够大的输入,#3有时可以赢得您的原始代码,至少在Python3.5上(我在ipython中使用了微基准),这取决于许多因素。它并不总是赢,因为字节码解释器中对二进制模
的值进行了一些优化,这些值可以直接跳转到整数的CPU寄存器中,但它的性能通常非常相似。我认为在一台机器上对一个小样本进行性能测试并不意味着速度可能会更快。@Fredrarson不是我想问的,而是回答你的评论。我之前已经实现了它,从我的测试来看,如果你能正确地猜出筛子大小的一个好的上限,它的速度会更快。否则,这更像是一场赌博。@cricket_007我已经做了好几次这个测试,得到了一致和相似的答案。在~0.01秒内。加上一秒,您可以很好地使用for else
:-)谢谢您的输入!有趣的是,在我加入sqrt_测试后,它确实运行得更快,但仍然没有比Lazysieve更快。13%快到4.23秒。对不起,第一个问题是关于堆栈的,仍然在弄清楚这一切是如何工作的!它记录了升级投票,但无法显示,因为我的级别太低。很抱歉被引用了,找不到推荐的方法,所以我只是猜测一下。不用担心,很高兴我能帮上一点忙。我没有想到这一点,但答案是有意义的。我将进行更多的调查,看看是否发生了这种情况。它不是每次都是一个新的堆栈帧,没有支付Python级别的函数调用开销。有一个新的堆栈帧(所有生成器表达式都涉及的单个帧),但在每次迭代中它都不是新的(说明性地,当每个值yield
ed时,堆栈帧被保存和恢复,但保存/恢复的成本远低于正常调用Python函数的成本,因为生成器协议经过了大量优化)@ShadowRanger知道这一点很酷。然而,帧之间的切换不应该有一些开销吗?有,但这只是等式的一部分。加载代码对象,在其上创建闭包,调用闭包(创建初始帧),然后加载all
(LEGB中的B
是一个杀手)并调用它,所有这一切都会增加设置开销,使小循环的实际工作无法完成,并在每次迭代中执行非局部变量的查找(相当于Python级dict
lookup与用于访问局部变量的简单C级固定数组索引查找),这增加了“每个循环”的工作。是的,这样做了。当使用进行test in(test,)
hack时,我没有注意到性能的变化。但是映射(tes
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
%%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