eratosthenes_Eratosthenes的筛子

eratosthenes

最近,我偶然发现Reddit线程指向一个存储库 ,该存储库比较了不同语言的Eratosthenes筛网实现的性能。 简而言之,Sieve是一种(古老的)算法,可以查找所有达到指定极限的素数。

至少可以说,结果令人着迷:

语言 性能(秒)

C

0.501

clang-fblocks-o prime_c prime.c
time ./prime_c &>/dev/null
real    0m0.501s
user    0m0.490s
sys     0m0.006s

Go 1.11

8.915

go build-o prime_go prime.go
time ./prime_go &>/dev/null
real    0m8.915s
user    0m31.161s
sys     0m0.227s

Python 3.6

10.85

time  python3 prime.py &>/dev/null
real    0m10.850s
user    0m10.739s
sys     0m0.066s

Ruby 2.4

481.470

time  ruby prime.rb &>/dev/null
real    8m1.470s
user    7m46.913s
sys     0m3.977s

Kotlin 1.3

2034.561

kotlinc prime.kt-include-runtime-d prime_kt.jar
time  java -jar prime_kt.jar &>/dev/null
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]
}
  1. 并行运行块的指令
  2. 将协程的计算结果合并到索引数组中

上面的代码在我的笔记本电脑上执行约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
    }
  }
}
  1. 在块中并行运行指令
  2. 将共享的可变数组传递给协程
  3. 从协程更新共享的可变数组

在我的机器上运行上述代码大约需要4毫秒。 比我的原始基准高出50%!

结论

就像标准Java并发编程API一样,协程也不保证任何事情。 它们应与正确的数据结构一起使用。 序列是惰性的,但是如果每次迭代都调用它们,它们会比渴望的可变对象慢。

这篇文章的完整源代码可以在Github上找到。

翻译自: https://blog.frankel.ch/sieve-eratosthenes/

eratosthenes

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值