eratosthenes
最近,我偶然发现Reddit线程指向一个存储库 ,该存储库比较了不同语言的Eratosthenes筛网实现的性能。 简而言之,Sieve是一种(古老的)算法,可以查找所有达到指定极限的素数。
至少可以说,结果令人着迷:
语言 | 性能(秒) | |
---|---|---|
C | 0.501 |
real 0m0.501s user 0m0.490s sys 0m0.006s |
Go 1.11 | 8.915 |
real 0m8.915s user 0m31.161s sys 0m0.227s |
Python 3.6 | 10.85 |
real 0m10.850s user 0m10.739s sys 0m0.066s |
Ruby 2.4 | 481.470 |
real 8m1.470s user 7m46.913s sys 0m3.977s |
Kotlin 1.3 | 2034.561 |
real 33m54.561s user 33m18.257s sys 0m11.193s |
最大(也是最糟糕)的惊喜来自Kotlin。 我简直不敢相信我的眼睛!
然后,我看了一下代码:
funmain(args:Array<String>){
funmake_sieve(src:Sequence<Int>,prime:Int)=src.filter{it%prime!=0}
varsieve=sequence{
varx=2
while(true)yield(x++)
}
for(iin1..10000){
valprime=sieve.first()
println(prime)
sieve=make_sieve(sieve,prime)
}
}
经过Redditors的一些评论后,作者将Sequence
替换为Iterator
,并且更新后的代码能够在2秒钟内运行。 仍然很多 。 我相信可以做得更好,所以让我们做吧。
天真的参考实现
在尝试做得更好之前,让我们以伪代码回到算法本身:
Input : an integer n > 1. Let A be an array of Boolean values, indexed by integers 2 to n , initially all set to true. for i = 2, 3, 4, …, not exceeding √n : if A[i] is true: for j = i2 , i2+i , i2+2i , i2+3i , …, not exceeding n : A[j] := false. Output : all i such that A[i] is true.
为了进行比较,需要参考实现。 以下代码片段是上述Kotlin中天真的简单算法的直接翻译:
funsieveSimple(n:Int):Array<Boolean>{
valindices=Array(n){true}
vallimit=sqrt(n.toDouble()).toInt()
valrange=2..limit
for(iinrange){
if(indices[i]){
varj=i.toDouble().pow(2).toInt()
while(j<n){
indices[j]=false
j+=i
}
}
}
returnindices
}
在我的机器上运行此代码大约需要9毫秒。 这将是改进的基准。
协程初稿
Kotlin协程允许并发运行代码。 一种简单的优化方法是在协程中针对特定i
进行计算,并将结果汇总到一个专用数组中。
funsieveCoroutines(n:Int):Array<Boolean>{
valindices=Array(n){true}
vallimit=sqrt(n.toDouble()).toInt()
valrange=2..limit
runBlocking{
async{ (1)
for(iinrange){
valresult=computeForSingleNumber(i,n)
mergeBooleanArrays(indices,result) (2)
}
}.await()
}
returnindices
}
funcomputeForSingleNumber(i:Int,n:Int):Array<Boolean>{
valindices=Array(n){true}
if(indices[i]){
varj=i.toDouble().pow(2).toInt()
while(j<n){
indices[j]=false
j+=i
}
}
returnindices
}
funmergeBooleanArrays(a1:Array<Boolean>,a2:Array<Boolean>){
valrange=0untila1.size
for(iinrange)a1[i]=a1[i]&&a2[i]
}
- 并行运行块的指令
- 将协程的计算结果合并到索引数组中
上面的代码在我的笔记本电脑上执行约300毫秒。 虽然比原始海报的代码要好得多,但是它比上述基准花费了30倍以上! 协程不应该受到谴责。
每次协程运行后合并结果是需要时间的块。
协程和共享的可变状态
每次运行后合并不同的阵列不是可行的方法。
显然,计算不相互依赖:一次计算就足以消除潜在的质数。 可能发生的最糟糕的情况是,两个不同的计算会删除相同的数字,而这不会改变结果。
因此,可以在所有协程之间安全地共享该数组。 更新后的代码如下所示:
funsieveSharedMutableState(n:Int):Array<Boolean>{
valindices=Array(n){true}
vallimit=sqrt(n.toDouble()).toInt()
valrange=2..limit
runBlocking{
async{ (1)
for(iinrange){
computeForSingleNumber(i,indices) (2)
}
}.await()
}
returnindices
}
funcomputeForSingleNumber(i:Int,indices:Array<Boolean>){
valn=indices.size
if(indices[i]){
varj=i.toDouble().pow(2).toInt()
while(j<n){
indices[j]=false (3)
j+=i
}
}
}
- 在块中并行运行指令
- 将共享的可变数组传递给协程
- 从协程更新共享的可变数组
在我的机器上运行上述代码大约需要4毫秒。 比我的原始基准高出50%!
结论
就像标准Java并发编程API一样,协程也不保证任何事情。 它们应与正确的数据结构一起使用。 序列是惰性的,但是如果每次迭代都调用它们,它们会比渴望的可变对象慢。
eratosthenes