为什么这个scala prime代这么慢/内存密集?

时间:2011-07-23 17:33:52

标签: scala performance out-of-memory primes sieve-of-eratosthenes

我在找到第10,001个素数时内存不足。

object Euler0007 {
  def from(n: Int): Stream[Int] = n #:: from(n + 1)
  def sieve(s: Stream[Int]): Stream[Int] = s.head #:: sieve(s.filter(_ % s.head != 0))
  def primes = sieve(from(2))
  def main(args: Array[String]): Unit = {
    println(primes(10001))
  }
}

这是因为在primes的每次“迭代”(这是这个上下文中的正确术语吗?)之后,我增加了要调用的函数堆栈以使其获得下一个元素吗?

我在网上找到的一种解决方案是this(问题7):{{3}}(问题7):我不想求助于迭代解决方案(我想避免进入函数式编程/惯用语句)。

lazy val ps: Stream[Int] = 2 #:: Stream.from(3).filter(i => ps.takeWhile(j => j * j <= i).forall(i % _ > 0))

从我所看到的,这不会导致这种类似递归的方式。这是一个很好的方法吗,或者你知道更好的方法吗?

3 个答案:

答案 0 :(得分:15)

这种缓慢的一个原因是它不是 Eratosthenes的筛子。阅读http://www.cs.hmc.edu/~oneill/papers/Sieve-JFP.pdf以获得详细说明(示例在Haskell中,但可以直接翻译成Scala)。

我对欧拉问题#7的旧解决方案也不是“真正的”筛子,但似乎对小数字来说效果不错:

object Sieve {

    val primes = 2 #:: sieve(3)

    def sieve(n: Int) : Stream[Int] =
          if (primes.takeWhile(p => p*p <= n).exists(n % _ == 0)) sieve(n + 2)
          else n #:: sieve(n + 2)

    def main(args: Array[String]) {
      println(primes(10000)) //note that indexes are zero-based
    }
}

我认为您的第一个版本的问题是您只有def s而没有val来收集结果,并且可以通过生成函数进行查询,因此您始终可以从头开始重新计算。< / p>

答案 1 :(得分:8)

是的,它是 ,因为你“增加了要调用的函数堆栈以获取下一个元素,在每个”迭代“之后加一个元素“ - 即每次获得每个素数后,每次在过滤器堆栈顶部添加一个新过滤器。那是过多的过滤器

这意味着每个生成的素数都会被其所有前面的素数进行测试 - 但只有那些低于其平方根的素数才真正需要。例如,要获得第10001个素数,104743,将在运行时创建10000个过滤器。但是323以下只有66个素数,104743的平方根,所以只需要66个过滤器。所有9934人都会在那里毫无必要地占据记忆,努力工作,完全没有附加价值。

这个 是“功能筛”的关键缺陷,它似乎起源于David Turner在20世纪70年代的代码中,后来进入{ {3}}和其他地方。 它是试验分区筛子(而不是 Eratosthenes的筛子)。那是 太远了 对它的关注。试验分割在最佳实施时,能够非常快速地产生第10000个素数。

该代码的主要缺点是它不会在适当的时刻推迟创建过滤器,最终会创建过多的过滤器。

现在谈论复杂性,“旧筛”代码是 O(n 2 生成n素数。最佳试验区分为 O(n 1.5 / log 0.5 (n)),Eratosthenes筛为 O(n *的log(n)*日志(日志(N)))。作为the SICP book,第一个通常被视为~ n^2,第二个被视为~ n^1.45,第三个被视为~ n^1.2

您可以找到基于Python生成器的代码,以实现最佳试验分区empirical orders of growth。是in this answer (2nd half of it)处理Haskell等效的筛选功能。


就像一个例子,旧筛子的originally discussed here :)是

primes = sieve [2..]  where
   sieve (x:xs) = x : sieve [ y | y <- xs, rem y x > 0 ]
                         -- list of 'y's, drawn from 'xs',
                         --         such that (y % x > 0)

并且用于最佳试验分割(TD)筛,在素数方块上同步,

primes = sieve [2..] primes   where
  sieve (x:xs) ps = x : (h ++ sieve [ y | y <- t, rem y p > 0 ] qs)
          where
            (p:qs) = ps     -- 'p' is head elt in 'ps', and 'qs' the rest
            (h,t)  = span (< p*p) xs    -- 'h' are elts below p^2 in 'xs'
                                        -- and 't' are the rest

"readable pseudocode"a sieve of Eratosthenes,如此处另一个答案中提到的JFP文章所示,

primes = 2 : minus [3..] 
               (foldr (\p r-> p*p : union [p*p+p, p*p+2*p..] r) [] primes)
                      -- function of 'p' and 'r', that returns 
                      -- a list with p^2 as its head elt, ...

快。 (devised by Richard Bird是一个列表a,其中b的所有元素都被逐渐删除; minus a b是一个列表a,其所有元素都为b渐进地添加到它没有重复;两者都处理有序的,非减少列表)。 foldr是列表的union a b。因为它是线性的,所以它在~ n^1.33运行,为了使其在~ n^1.2运行,可以使用the right fold函数tree-like folding


第二个问题的答案也是 。你的第二个代码,用相同的“伪代码”重写,

ps = 2 : [i | i <- [3..], all ((> 0).rem i) (takeWhile ((<= i).(^2)) ps)]

与上面的最佳TD筛非常相似 - 两者都安排每个候选者通过其平方根以下的所有质数进行测试。虽然筛子使用推迟过滤器的运行时序列来安排,但后一定义为每个候选者重新获取所需的素数。一个可能比另一个更快,具体取决于编译器,但两者基本相同。

第三个也是 :Eratosthenes的筛子更好,

ps = 2 : 3 : minus [5,7..] (unionAll [[p*p, p*p+2*p..] | p <- drop 1 ps])

unionAll = foldi union' []          -- one possible implementation
union' (x:xs) ys = x : union xs ys
   -- unconditionally produce first elt of the 1st arg 
   -- to avoid run-away access to infinite lists

看起来它也可以在Scala中实现,从其他代码片段的相似性来判断。 (虽然我不知道斯卡拉)。 unionAll这里实现了树状折叠结构foldi,但也可以使用滑动数组实现,沿着素数流的倍数逐段工作。

TL; DR: 是,是,是的。

答案 2 :(得分:6)

FWIW,这是一个真正的Eratosthenes筛子:

def sieve(n: Int) = (2 to math.sqrt(n).toInt).foldLeft((2 to n).toSet) { (ps, x) => 
    if (ps(x)) ps -- (x * x to n by x) 
    else ps
}

这是一个无限的素数流,使用了Eratosthenes筛子的变体来保留其基本属性:

case class Cross(next: Int, incr: Int)

def adjustCrosses(crosses: List[Cross], current: Int) = {
  crosses map {
    case cross @ Cross(`current`, incr) => cross copy (next = current + incr)
    case unchangedCross                 => unchangedCross
  }
}

def notPrime(crosses: List[Cross], current: Int) = crosses exists (_.next == current)

def sieve(s: Stream[Int], crosses: List[Cross]): Stream[Int] = {
    val current #:: rest = s

    if (notPrime(crosses, current)) sieve(rest, adjustCrosses(crosses, current))
    else current #:: sieve(rest, Cross(current * current, current) :: crosses)
}

def primes = sieve(Stream from 2, Nil)

然而,这有点难以使用,因为Stream的每个元素都是使用crosses列表组成的,其中包含的数字与数字的数量一样多,并且它似乎由于某种原因,这些列表被保存在Stream中的每个数字的内存中。

例如,在评论的提示下,primes take 6000 contains 56993会抛出GC异常,而primes drop 5000 take 1000 contains 56993会在我的测试中返回相当快的结果。