def sieve(upperlimit):
# mark off all multiples of 2 so we can use 2*p as the step for the inner loop
l = [2] + [x if x % 2 != 0 else 0 for x in range(3, upperlimit + 1)]
for p in l:
if p ** 2 > upperlimit:
break
elif p:
for i in range(p * p, upperlimit + 1, 2 * p):
l[i - 2] = 0
# filter out non primes from the list, not really that important i could work with a list full of zeros as well
return [x for x in l if x]
#1 楼
这里是计算性能改进的起点,这归因于以下各种修订:筛选\ $ 10 ^ 8 \ $以下的质数需要多长时间?
>>> from timeit import timeit
>>> test = lambda f: timeit(lambda:f(10**8), number=1)
>>> t1 = test(sieve)
确切的数字将取决于计算机的速度,因此我将计算性能比率,但记录在这里:
>>> t1
78.9875438772142
您的列表
l
的初始化花费了一半以上的时间,因此让我们尝试一种更便宜的方法。我们还要给这个数组起一个更好的名称,并在使用时将其设为布尔型数组。def sieve2(n):
"""Return a list of the primes below n."""
prime = [True] * n
for p in range(3, n, 2):
if p ** 2 > n:
break
if prime[p]:
for i in range(p * p, n, 2 * p):
prime[i] = False
return [2] + [p for p in range(3, n, 2) if prime[p]]
优化此类函数时,始终值得保留围绕未优化版本检查优化版本的正确性:
>>> sieve(10**6) == sieve2(10**6)
True
此已经运行了不到三分之一的时间:
>>> test(sieve2) / t1
0.30390444573149544
我们可以通过计算更严格的循环限制来避免对
p ** 2 > n
进行测试。请注意,我在这里使用了n ** .5
,因为它比math.sqrt(n)
快一点。def sieve3(n):
"""Return a list of the primes below n."""
prime = [False, False, True] + [True, False] * (n // 2)
for p in range(3, int(n ** .5) + 1, 2):
if prime[p]:
for i in range(p * p, n, 2 * p):
prime[i] = False
return [p for p in range(2, n) if prime[p]]
这与总体运行时间没有什么区别:
>>> test(sieve3) / t1
0.2971086436068156
我们可以随时累积结果,而不必在最后进行单独的迭代。请注意,我已将
result.append
缓存在局部变量中,以避免每次在循环中查找它。def sieve4(n):
"""Return a list of the primes below n."""
prime = [False, False, True] + [True, False] * (n // 2)
result = [2]
append = result.append
sqrt_n = (int(n ** .5) + 1) | 1 # ensure it's odd
for p in range(3, sqrt_n, 2):
if prime[p]:
append(p)
for i in range(p * p, n, 2 * p):
prime[i] = False
for p in range(sqrt_n, n, 2):
if prime[p]:
append(p)
return result
同样,这几乎没有什么区别:
>>> test(sieve4) / t1
0.286016401170129
将筛子条目设置为
False
时,可以使用Python的切片分配而不是循环。这看起来很浪费,因为我们先创建了一个大列表然后将其丢弃,但这避免了昂贵的for
循环和相关的Python解释器开销。但有明显的改进: >>> test(sieve5) / t1
0.2617646381557855
对于数值性能有很大的改进代码,我们可以使用NumPy。
def sieve5(n):
"""Return a list of the primes below n."""
prime = [True] * n
result = [2]
append = result.append
sqrt_n = (int(n ** .5) + 1) | 1 # ensure it's odd
for p in range(3, sqrt_n, 2):
if prime[p]:
append(p)
prime[p*p::2*p] = [False] * ((n - p*p - 1) // (2*p) + 1)
for p in range(sqrt_n, n, 2):
if prime[p]:
append(p)
return result
这是
sieve5
的6倍以上,是原始代码的25倍以上: >>> test(sieve6) / t1
0.03726392181902129
我们可以避免为偶数分配空间,从而改善了内存局部性:
import numpy
def sieve6(n):
"""Return an array of the primes below n."""
prime = numpy.ones(n, dtype=numpy.bool)
prime[:2] = False
prime[4::2] = False
sqrt_n = int(n ** .5) + 1
for p in range(3, sqrt_n, 2):
if prime[p]:
prime[p*p::2*p] = False
return prime.nonzero()[0]
>>> test(sieve7) / t1
0.029220096670965198
最后,一个实现会分别筛选\ $ 6i−1 \ $和\ $ 6i + 1 + \ $质数的实现,这要归功于Robert William Hanks:
def sieve7(n):
"""Return an array of the primes below n."""
prime = numpy.ones(n // 2, dtype=numpy.bool)
sqrt_n = int(n ** .5) + 1
for p in range(3, sqrt_n, 2):
if prime[p // 2]:
prime[p*p // 2::p] = False
result = 2 * prime.nonzero()[0] + 1
result[0] = 2
return result
这是原始
sieve
的约40倍: >>> test(sieve8) / t1
0.023447068662434022
#2 楼
您将所有偶数都设置为0的第一个方法不是很有效,筛分的全部目的是避免那些昂贵的模运算。请尝试以下操作:l = range(2, upperlimit+1) # use list(range(...)) in Python 3
l[2::2] = [0] * ((len(l) - 3) // 2 + 1)
对于其他质数的筛子零点的设置,您可以执行类似的操作,但是要弄清楚有多少个零点会很复杂。在右边添加。
评论
\ $ \ begingroup \ $
哦,好极了,是的,在生成列表时节省了我很多时间
\ $ \ endgroup \ $
– Igglyboo
2014年2月21日在16:27
\ $ \ begingroup \ $
请注意,此操作在python3中失败(问题被标记为python3)。
\ $ \ endgroup \ $
–巴库留
2014年2月21日在17:22
\ $ \ begingroup \ $
是的,Python 3需要将范围包装在列表中才能正常工作,并在上面添加了注释。
\ $ \ endgroup \ $
– Jaime
2014年2月21日在17:59
#3 楼
您已经正确实现了优化p ** 2 > upperlimit
,但是,有一种更有效的方法可以执行此优化。该产品不是很昂贵,但也完全没有必要。...您可以只计算一次上限的平方根,然后重用该计算值作为对
p ** 2 > upperlimit
的直接比较。 。考虑:rootlimit = math.sqrt(upperlimit)
for p in l:
if p > rootlimit:
break;
....
另外,这是一件小事,但是如果您在循环中的条件循环中包含
p
,p
或break
,则无需使用continue
...只是一件小事,但是您的代码可能是:rootlimit = math.sqrt(upperlimit)
for p in l:
if p > rootlimit:
break;
if p:
....
#4 楼
for p in l:
if p ** 2 > upperlimit:
break
elif p:
...
在此循环中,
p
通常为零,而p ** 2 > upperlimit
始终为false,直到break
为止。这意味着您总是要评估两个条件。如果将if p:
放在第一位,则只要p
不是素数,就只会评估一个条件。
评论
\ $ \ begingroup \ $
可以使用滚轮分解对7进行一般化,这将为您带来另一项重大改进。根据我的经验,将筛子装满适合L1或L2缓存的块会增加另一个因素5或更多
\ $ \ endgroup \ $
–尼古拉斯B.
2014年2月22日,0:14
\ $ \ begingroup \ $
@NiklasB .:听起来很有趣!您可以发布代码吗?
\ $ \ endgroup \ $
–加雷斯·里斯(Gareth Rees)
2014-2-22在17:09
\ $ \ begingroup \ $
@GarethRees:我曾经为在线法官问题写过一个实现:pastie.org/8758862它在一秒钟之内就能在机器上计算π(10 ^ 9),但请耐心等待,它经过了高度优化和优化丑陋的家伙...如果我没记错的话,它还包含一些其他自定义优化,这些优化我没有提到。在那麻木的实施上做得很好,它的速度非常快。我想我高估了可以克服的速度。真的很酷
\ $ \ endgroup \ $
–尼古拉斯B.
2014-02-22 17:27