def sieve(n):
nums = [0] * n
for i in range(2, int(n**0.5)+1):
if nums[i] == 0:
for j in range(i*i, n, i):
nums[j] = 1
return [i for i in range(2, n) if nums[i] == 0]
def sieve_var(n):
nums = [0] * n
for i in range(3, int(n**0.5)+1, 2):
if nums[i] == 0:
for j in range(i*i, n, i):
nums[j] = 1
return [2] + [i for i in range(3, n, 2) if nums[i] == 0]
在我的机器上,sieve(10**8)
需要2.28秒而sieve_var(10**8)
需要2.67秒。我不认为pypy的预热时间是这里的罪魁祸首,所以为什么不是sieve_var
,它会越来越快地迭代?在标准的python 3.3中,sieve_var
的速度比预期的要快。在Windows 8.1上使用pypy 4.0.1 32位。
编辑:作为测试,我在函数的开头添加了count = 0
,在内部循环中添加了count += 1
(其中nums[j] = 1
是)。 sieve(10**8)
计算为242570202,而sieve_var(10**8)
计为192570204.因此虽然sieve_var
的计数不会减半,但它的工作量却减少了#34;工作量为#34;
答案 0 :(得分:10)
我不确定为什么它在Windows上会稍微慢一些。在Linux上速度是一样的。但是,我可以回答为什么我们大多数的速度相同。如果程序是用C语言编写的,答案是相同的,答案纯粹是在处理器级别。该程序绑定在访问列表的内存I / O上,大小为400或800MB。在第二个版本中,您基本上避免了额外的if nums[i] == 0
检查。但是,这个额外的检查没有任何成本,因为CPU在上一次迭代期间只在其缓存中获取nums[i - 1]
,并且在下一次迭代期间需要nums[i + 1]
。无论如何,CPU正在等待内存。
要验证我所说的内容,请尝试使nums
数组更紧凑。我尝试使用nums[i // 2]
访问它,假设i
总是奇数,结果快了两倍。你可以通过不使用Python列表(在32位PyPy上存储为32位整数数组)来赢得更多,而是一个位数组(但它的代码更多,因为没有标准的内置位数组。)
答案 1 :(得分:4)
TL,DR;
作为C程序,这将是一个内存绑定算法。然而,即使是jit编译的pypy-code也有相当多的开销,而且操作不再是免费的#34;。令人惊讶的(或者可能不是),两个sieve
版本的版本具有不同的jit代码,第二个版本导致代码速度变慢可能只是运气不好。
如果是C,@ Armin的回答是正确的。众所周知,对于现代计算机/缓存和内存绑定代码,如果我们跳过一个整数并不重要 - 尽管如此,所有值都必须从内存中获取,这是一个瓶颈。有关详细说明,请参阅this article。
然而,我的实验表明,非优化版本(sieve
)比优化版本(sieve_var
)略快。 Timings还表明,sieve
的最后一行([i for i in range(2, n) if nums[i] == 0]
的执行速度比sieve_var
- return [2] + [i for i in range(3, n, 2) if nums[i] == 0]
的执行速度快。
在我的计算机上,0.45
元素的0.65
秒与10^8
秒相比。这些数字可能因机器而异,因此很有可能拥有更快CPU和更慢内存的人根本看不到任何差异。如果它可以用"内存支配所有"来解释,那么我们应该能够看到,较慢的版本有更多的缓存未命中作为更快的版本。
但是,通过运行valgrind --tool=cachegrind pypy sieveXXX.py
,我们可以看到,缓存未命中数几乎没有差异,至少没有任何可以解释可观察差异的内容。
让我们考虑一个稍微简单的版本,它表现出完全相同的行为 - 我们不会保存素数,但只计算它们:
def sieve(n):
...
res=0
for i in range(2, n):
if nums[i] == 0:
res+=1
return res
def sieve_var(n):
...
res=1
for i in range(3, n,2):
if nums[i] == 0:
res+=1
return res
第一个版本仍然更快:0.35
秒。与0.45
秒相比(为了确保时间差异不是侥幸而不是由于某些jit-warmup,我将代码的最后部分放入for循环并且总是得到相同的时间)。
在进一步讨论之前,让我们看一下C实现及其程序集
long long int sum(long long int *a, int n){
long long int res=0;
for(int i=2;i<n;i++)
if(a[i]==0)
res++;
return res;
}
使用gcc and -Os
we get编译:
movl $2, %edx
xorl %eax, %eax
.L4:
cmpl %edx, %esi
jle .L1
cmpq $0, (%rdi,%rdx,8)
jne .L3
incq %rax
.L3:
incq %rdx
jmp .L4
.L1:
ret
非常小而直接,我的机器只需0.08
秒。我的内存可以快到10 GB / s并且有8*10^8
个字节 - 因此基本上需要整个时间来获取数据。
但是从这一点我们也看到,与C代码相比,pypy版本的开销大约为0.25
秒。它从何而来?通过使用vmprof-module,我们可以看到jit-code和:
sieve
和sieve_par
的版本非常不同。我们可以use debugger to count the number of instruction进行迭代:24
用于sieve
和76
- sieve_var
的操作仅处理每一个元素,因此关系实际为{{} 1}}。 很难说,为什么jit-code对于没有调试pypy的两个版本都是如此不同。可能只是运气不好,24:38
速度较慢。