object PrimesEratosthenesSieveApp extends App {
PrimesEratosthenesSieve.println(primes(1000))
PrimesEratosthenesSieve.println(primes(1000).length)
}
运行结果:
168
List(2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997)
简直不费吹灰之力。
我们来测试一下这个函数的性能。首先,从 wikipedia 上查到我们需要的素数个数,做一些准备工作:
package
import org.openjdk.jmh.annotations.{Benchmark, Param, Scope, State}
@State(Scope.Benchmark)
class PrimeState {
@Param(Array("1000", "10000", "100000", "1000000", "10000000", "100000000", "1000000000")) var n: Int = 0
}
trait PrimesBenchmark {
protected final val p1e3 = 1000
protected final val p1e4 = 10000
protected final val p1e5 = 100000
protected final val p1e6 = 1000000
protected final val p1e7 = 10000000
protected final val p1e8 = 100000000
protected final val p1e9 = 1000000000
protected final def primesMap: Map[Int, Int] = Map(
p1e3 -> 168,
p1e4 -> 1229,
p1e5 -> 9592,
p1e6 -> 78498,
p1e7 -> 664579,
p1e8 -> 5761455,
p1e9 -> 50847534
)
def bench(n: Int, f: Int => Int): Unit = assert(primesMap.get(n).contains(f(n)))
}
定义测试类:
class EratosthenesSieveBenchmark extends PrimesBenchmark {
@Benchmark def primes1(state: PrimeState): Unit = bench(state.n, PrimesEratosthenesSieve.primes1(_).length)
}
开始测试:
$ JAVA_OPTS="-Xmx8G -Xms8G -XX:+UseG1GC" sbt "bench/Jmh/run -bm avgt -i 3 -wi 1 -f 1 -t 1 simple.EratosthenesSieveBenchmark"
参数解释:
堆内存,固定 8GB
垃圾回收算法 G1GC
测试模式 avgt, 应该设为平均运行时间,而不是吞吐
Blackhole mode: full + dont-inline hint
Warmup: 1 iterations, 10 s each
Measurement: 3 iterations, 10 s each
Threads: 1 thread, will synchronize iterations
硬件环境:
软件环境:
测试结果:
[info] Benchmark (n) Mode Cnt Score Error Units
[info] EratosthenesSieveBenchmark.primes1 1000 avgt 3 ≈ 10⁻⁵ s/op
[info] EratosthenesSieveBenchmark.primes1 10000 avgt 3 ≈ 10⁻⁴ s/op
[info] EratosthenesSieveBenchmark.primes1 100000 avgt 3 0.004 ± 0.001 s/op
[info] EratosthenesSieveBenchmark.primes1 1000000 avgt 3 0.086 ± 0.005 s/op
[info] EratosthenesSieveBenchmark.primes1 10000000 avgt 3 1.822 ± 0.017 s/op
[info] EratosthenesSieveBenchmark.primes1 100000000 avgt 3 42.306 ± 2.556 s/op
[info] EratosthenesSieveBenchmark.primes1 1000000000 avgt 3 1027.604 ± 20.288 s/op
可见,当 n 等于 1 亿的时候,需要 42 秒,等于 10 亿的时候需要 1027 秒(17分钟)。
众所周知埃拉托色尼筛的时间复杂度是 O(nlnln(n)), 但这个简单粗糙的实现显然要低效得多。至于具体的复杂度估算,我现在也不会(目测并不那
么简单)。但是我们可以简单地加上点调试代码,以便和后面的改进算法对比:
def primes1c(n: Int): List[Int] = {
if (n <= 7) List(2, 3, 5, 7).takeWhile(_ <= n)
else {
val q = math.sqrt(n.toDouble).toInt
val qs = primes1c(q)
var c = 0L
val r = qs ++ (q + 1 to n).filter { x =>
qs.forall { q =>
c += 1L
x % q != 0
}
}.toList
println(s"n = $n, c = $c")
r
}
}
println(println(primes1c(100000000).length))
容易想到的,可以改用一个 BitSet 来存储筛选的结果,这样整个算法过程会更接近当年手写素数的场景,遍历的数字会更少,占用的内存也会更小。同时,我们把函数的返回值从 List 改为 Iterator.
def primes2(n: Int): Iterator[Int] = {
if (n <= 7) Iterator(2, 3, 5, 7).takeWhile(_ <= n)
else {
val q = math.sqrt(n.toDouble).toInt
val qs = primes2(q)
val bs = collection.mutable.BitSet.empty
qs.foreach { q =>
(2 to n / q).foreach { p =>
bs.addOne(p * q)
}
}
Iterator.range(2, n).filterNot(bs.apply)
}
}
class EratosthenesSieveBenchmark extends PrimesBenchmark {
@Benchmark def primes2(state: PrimeState): Unit = bench(state.n, PrimesEratosthenesSieve.primes2(_).length)
}
结果:
[info] EratosthenesSieveBenchmark.primes2 1000 avgt 3 ≈ 10⁻⁵ s/op
[info] EratosthenesSieveBenchmark.primes2 10000 avgt 3 ≈ 10⁻⁴ s/op
[info] EratosthenesSieveBenchmark.primes2 100000 avgt 3 0.001 ± 0.001 s/op
[info] EratosthenesSieveBenchmark.primes2 1000000 avgt 3 0.014 ± 0.023 s/op
[info] EratosthenesSieveBenchmark.primes2 10000000 avgt 3 0.109 ± 0.009 s/op
[info] EratosthenesSieveBenchmark.primes2 100000000 avgt 3 1.245 ± 0.382 s/op
[info] EratosthenesSieveBenchmark.primes2 1000000000 avgt 3 15.494 ± 17.753 s/op
当 n 等于 1 亿的时候,只需要 1.2 秒,等于 10 亿的时候只需要 16 秒。没想到看似低效的手工“划掉”,居然比现在比较流行的算法快 60 倍。
下面来分析一下这个筛法的复杂度。显而易见,应该是
n(21+31+51+71+111+…)=np prime∑p1
根据 wiki,
p≤n∑p1∼lnln(n+1)−ln6π2
因此朴素埃拉托色尼筛的算法复杂度应该是 O(nlnln(n)) .
简单验证一下:
def primes2c(n: Int): Iterator[Int] = {
if (n <= 7) Iterator(2, 3, 5, 7).takeWhile(_ <= n)
else {
val q = math.sqrt(n.toDouble).toInt
val qs = primes2(q)
val bs = collection.mutable.BitSet.empty
var c = 0L
qs.foreach { q =>
(2 to n / q).foreach { j =>
c += 1L
bs.addOne(q * j)
}
}
println(s"n = $n, c = $c")
Iterator.range(2, n).filterNot(bs.apply)
}
}
println(primes2c(100000000).length)
可见 n=108 时, primes2 循环次数为 248304142, 而 primes1 为 8576796314, 差距为 34.5 倍。
而 108lnln(108)=291347398, 与实际的循环次数差距仅有 17.
primes2 最后输出结果,使用了一个 Range (2 to n) 加上 BitSet 来逐个过滤,会有不小的性能浪费。如果我们反过来,直接先填满 BitSet, 然后把
合数从中删掉,最后再直接用 BitSet 的迭代器输出,会怎么样呢?马上试试:
def primes3(n: Int): Iterator[Int] = {
if (n <= 7) Iterator(2, 3, 5, 7).takeWhile(_ <= n)
else {
val q = math.sqrt(n.toDouble).toInt
val qs = primes3(q)
val bs = collection.mutable.BitSet.fromSpecific(2 to n)
qs.foreach { q =>
(2 to n / q).foreach { p => bs.remove(p * q) }
}
bs.iterator
}
}
测试:
class EratosthenesSieveBenchmark extends PrimesBenchmark {
@Benchmark def primes3(state: PrimeState): Unit = bench(state.n, PrimesEratosthenesSieve.primes3(_).length)
}
结果:
[info] Benchmark (n) Mode Cnt Score Error Units
[info] EratosthenesSieveBenchmark.primes3 1000 avgt 3 ≈ 10⁻⁵ s/op
[info] EratosthenesSieveBenchmark.primes3 10000 avgt 3 ≈ 10⁻⁴ s/op
[info] EratosthenesSieveBenchmark.primes3 100000 avgt 3 ≈ 10⁻³ s/op
[info] EratosthenesSieveBenchmark.primes3 1000000 avgt 3 0.004 ± 0.001 s/op
[info] EratosthenesSieveBenchmark.primes3 10000000 avgt 3 0.048 ± 0.008 s/op
[info] EratosthenesSieveBenchmark.primes3 100000000 avgt 3 0.524 ± 0.286 s/op
[info] EratosthenesSieveBenchmark.primes3 1000000000 avgt 3 8.781 ± 1.521 s/op
结果比 primes2 有接近翻倍的性能提升。
至此,我们得到了朴素埃拉托色尼筛法的最佳实现:
package
import scala.collection.mutable
object PrimesEratosthenesSieve extends App {
def primes(n: Int): List[Int] = {
if (n <= 7) List(2, 3, 5, 7).takeWhile(_ <= n)
else {
val q = math.sqrt(n.toDouble).toInt
val qs = primes2(q)
val bs = mutable.BitSet.empty
qs.foreach { q =>
(2 to n / q).foreach { j =>
bs.addOne(q * j)
}
}
(2 to n).filterNot(bs.apply).toList
}
}
}
我们将会在下一篇里实现更高效的素数筛法。
附,我后来从 孔夫子旧书网 买回来的二手书:



