在Scala中,为什么我的Sieve算法运行得如此之慢?

时间:2011-10-27 05:01:35

标签: performance scala functional-programming

我正在尝试使用列表和过滤器而不是数组和循环来实现Eratosthenes的Sieve。我不确定为什么以下表现比命令式等效表现差得多。 100万应该绝对飞,但我的机器停止了。

  val max = 1000000
  def filterPrimes(upper: Int, seed: Int = 2, sieve: List[Int] = List()): List[Int] =
    sieve.map(x => if (x % seed == 0 && x > seed) 0 else x).filter(_ > 0)

  var filtered: List[Int] = (2 to max).toList
  for (i <- 2 to max / 2) filtered = filterPrimes(max, i, filtered)
  filtered.foreach(println(_))

5 个答案:

答案 0 :(得分:10)

如果您希望看到筛子的功能性方法,请查看The Genuine Sieve of Eratosthenes

答案 1 :(得分:10)

有一些潜在的问题,虽然我没有真正看到一支“吸烟枪”......无论如何,这就是我所拥有的。第一:

sieve.map(x => if (x % seed == 0 && x > seed) 0 else x).filter(_ > 0)

可以更简洁地写成:

sieve.filter(x => x <= seed || x % seed != 0)

接下来,upper中未使用filterPrimes(但这应该对性能没有影响)。

第三,如果你想真正使用纯函数式,请不要使用varfor循环,而是将filterPrimes转换为尾递归函数。如果你这样做,编译器可能足够巧妙地优化副本(尽管我不会屏住呼吸)。

最后,也许最重要的是,您的for循环浪费了大量时间来过滤掉必须已经过滤的值。例如,它在已经过滤了所有2的倍数后尝试过滤4的倍数。如果要有效地使用此筛选算法,则需要从列表中的其余元素中选择种子。

换句话说,将索引保留在列表中,并从索引中确定种子,例如:

iteration 0: 2 3 4 5 6 7 8 9 ...
      index: ^

iteration 1: 2 3 5 7 9 ...
      index:   ^

iteration 2: 2 3 5 7 ...
      index:     ^

这避免了重复的努力。此外,在到达max之前,您不需要继续进行迭代,我认为当您超过sqrt(max)时,您实际上可以停止。

答案 2 :(得分:5)

我会做一些修改。

  • filterPrimes2之间为所有数字执行max / 2似乎很奇怪,“实际”筛选技术要求您只执行{{1在filterPrimes2之间的所有素数
  • 使用var和for循环似乎很奇怪。要做到“功能”的方式,我会使用递归函数。
  • 您不必在整个列表中执行sqrt(max),而是可以随时收集素数;不需要一遍又一遍地通过过滤器。
  • filterPrimes然后map的做法相当奇怪,因为地图只是标记要过滤的元素,你只需使用过滤器即可完成相同的操作。

所以这是我第一次尝试这些修改:

filter

然而,这反映了我的Haskell偏见,并且有一个巨大的缺陷:由于def filterFactors(seed: Int, xs: List[Int]) = { xs.filter(x => x % seed != 0) } def sieve(max: Int) = { def go(xs: List[Int]) : List[Int] = xs match { case y :: ys => { if (y*y > max) y :: ys else y :: go(filterFactors(y, ys)) } case Nil => Nil } go((2 to max).toList) } 辅助函数中的递归调用y :: go(...),它将占用大量的堆栈空间。运行go会导致我出现“OutOfMemoryError”。

让我们尝试一个常见的FP技巧:使用累加器进行尾递归。

sieve(1000000)

通过添加累加器值,我们能够以尾递归形式编写def sieve(max: Int) = { def go(xs: List[Int], acc: List[Int]) : List[Int] = xs match { case y :: ys => { if (y*y > max) acc.reverse ::: (y :: ys) else go(filterFactors(y, ys), y :: acc) } case Nil => Nil } go((2 to max).toList, Nil) } 辅助函数,从而避免了之前的巨大堆栈问题。 (Haskell的评估策略非常不同;因此它既不需要也不会受益于尾递归)

现在让我们将速度与基于突变的命令式方法进行比较。

go

在这里,我使用def mutationSieve (max: Int) = { var arr: Array[Option[Int]] = (2 to max).map (x => Some (x)).toArray var i = 0 var seed = (arr (i)).get while (seed * seed < max) { for (j: Int <- (i + seed) to (max - 2) by seed) { arr (j) = None } i += 1 while (arr (i).isEmpty) { i += 1 } seed = (arr (i)).get } arr.flatten } ,并通过将其条目替换为“无”来“交叉”一个数字。有一点优化空间;也许通过使用bool数组可以获得小的速度提升,其中索引代表特定的数字。不管。

使用非常原始的技术(仔细放置Array[Option[Int]]次调用...)我将功能版本的基准测试比命令式版本慢大约6倍。很明显,这两种方法具有相同的大时间复杂度,但是使用链表进行编程所涉及的常数因素确实会产生成本。

我还使用new Date()代替Math.sqrt(max).ceil.toInt对您的版本进行了基准测试:它比我在此处提供的功能版本慢了约15倍。有趣的是,据估计1 1到1000之间的每7个数字中约有1个(max / 2)是素数(1 / ln(1000)),因此,很大一部分减速可归因于你在每个数字上执行循环这一事实,而我只为每个素数执行我的函数。当然,如果执行~1000次迭代需要15倍的时间,它执行500000次迭代需要大约7500倍,这就是为什么你原来的代码很慢的原因。

答案 3 :(得分:2)

这是一个快速的筛选,实现了合并冲突的提示以及Ken Wayne VanderL提到的论文中的一些提示:

def createPrimes (MAX: Int) : Array[Boolean] = {
  val pri = (false :: false :: true :: List.range (3, MAX + 1).map (_ % 2 != 0)).toArray
  for (i <- List.range (3, MAX)
    if (pri (i))) {
      var j = 2 * i;
      while (j < MAX) {
        if (pri (j))
          pri (j) = false;
        j += i;
      }
    }
  pri
}
val MAX = 1000*1000
(1 to MAX).filter (createPrimes (MAX))

比较图表: graph comparing 4 algos with different MAX-sizes 垂直轴显示秒,水平从100 000到1 000 000个素数。 deltaNovember算法已经改进,只能运行到math.sqrt(max)和过滤,由Alexey Romanov在评论中提出。来自Dan Burton我采用了第二种算法,最后一种算法进行了小修改,以适应我的接口(List,而不是Array)和bitSet Sieve,他只在评论中链接,但是速度最快。

答案 4 :(得分:0)

列表是不可变的,每次调用filterPrimes都会创建一个新列表。你正在创建很多列表,顺便说一下,这是不必要的。

以您的第一直觉(您可能称之为“命令式等效”),我猜测它使用单个可变数组。

(编辑说明我明白不需要创建多个列表。)