计算素数时堆栈空间溢出

时间:2012-08-12 22:23:37

标签: primes haskell

我正在通过Real World Haskell工作(我在第4章)并且练习了一下我已经创建了以下程序来计算第n个素数。

import System.Environment

isPrime primes test = loop primes test
    where
        loop (p:primes) test
            | test `mod` p == 0 = False
            | p * p > test = True
            | otherwise = loop primes test


primes = [2, 3] ++ loop [2, 3] 5
    where 
        loop primes test
            | isPrime primes test = test:(loop primes' test')
            | otherwise = test' `seq` (loop primes test')
            where
               test' = test + 2
               primes' = primes ++ [test]

main :: IO()
main = do
    args <- getArgs
    print(last  (take (read (head args) :: Int) primes))

显然,因为我正在保存素数列表,所以这不是一个恒定的空间解决方案。问题是,当我尝试获得一个非常大的素数./primes 1000000时,我收到错误:

    Stack space overflow: current size 8388608 bytes.
    Use `+RTS -Ksize -RTS' to increase 

我很确定我的尾递归正确;阅读http://www.haskell.org/haskellwiki/Stack_overflow以及这里的各种回答让我相信它是懒惰评估的副产品,并且在它溢出之前,thunk正在积累,但到目前为止我还没有成功修复它。我尝试在各个地方使用seq来强制进行评估,但它没有产生影响。我是在正确的轨道上吗?还有其他我没有得到的东西吗?

3 个答案:

答案 0 :(得分:6)

正如我在评论中所说,你不应该通过在一个很长的列表(你的行primes' = primes ++ [test])的末尾添加一个元素列表来构建一个列表。最好只定义无限列表primes,让懒惰的评估做到这一点。类似下面的代码:

primes = [2, 3] ++ loop 5
    where.
        loop test
            | isPrime primes test = test:(loop test')
            | otherwise = test' `seq` (loop test')
            where
                test' = test + 2

显然你不需要isPrime primes函数参数化,但这只是一个问题。此外,当您知道所有数字都是正数时,您应该使用rem而不是mod - 这会导致我的机器性能提高30%(当找到百万分之一的素数时)。

答案 1 :(得分:2)

首先,你没有尾递归,但守护递归,a.k.a。tail recursion modulo cons

正如其他人评论的那样,你获得堆栈溢出的原因是一堆垃圾堆积。但是哪里?一个建议的罪魁祸首是你使用(++)。虽然不是最优的,但(++)的使用不一定会导致thunk堆积和堆栈溢出。例如,调用

take 2 $ filter (isPrime primes) [15485860..]

应该立即生成[15485863,15485867],并且没有任何堆栈溢出。但它仍然是使用(++)的相同代码,对吧?

问题是,您有两个列表,您调用了primes 。一个(在顶层)是无限的,通过保护(非尾部)递归共同递归地产生。另一个(loop的一个参数)是一个有限列表,通过将每个新发现的素数添加到其末尾来构建,用于测试。

但是当它用于测试时,它并没有被强制执行到最后。如果发生这种情况,就不会有SO问题。 只强制通过测试号码的sqrt 。所以(++) thunks积累了那一点。

调用isPrime primes 15485863时,会强制顶级primes达到3935,这是547个素数。内部测试素数列表也包含547个素数,其中只有前19个被强制。

但是当你拨打primes !! 1000000时,重复内部列表中的1,000,000个素数中只有547个被强制使用。其余的都是砰的一声。

如果您只在候选人中看到 square 时在testing-primes列表的末尾添加新素数,则testing-primes列表将始终强制执行,或接近它的结束,并没有导致SO的thunk堆积。并且将(++)附加到强制列表的末尾并不是那么糟糕,当下一次访问强制列出到它的结尾并且不留下任何thunk。 (它仍然会复制列表。)

当然,顶级primes列表可以直接使用,正如Thomas M. DuBuisson在他的回答中所示。

但内部列表有其用途。如果正确实现,只有在候选者中看到正方形时才向其添加新素数,当使用优化进行编译时,它可能允许您的程序在 O(sqrt(n))空间中运行。

答案 2 :(得分:0)