双流馈送以防止不必要的记忆?

时间:2012-12-15 17:21:11

标签: haskell primes sieve-of-eratosthenes lazy-sequences space-leak

我是Haskell的新手,我正试图以流处理方式实现Euler的Sieve。

当我检查Haskell Wiki page about prime numbers时,我发现了一些神秘的流优化技术。 在该wiki的 3.8线性合并中:

primesLME = 2 : ([3,5..] `minus` joinL [[p*p, p*p+2*p..] | p <- primes']) 
  where
    primes' = 3 : ([5,7..] `minus` joinL [[p*p, p*p+2*p..] | p <- primes'])

joinL ((x:xs):t) = x : union xs (joinL t)

它说

  

此处引入双素数Feed以防止不需要   根据Melissa O'Neill的说法,记忆并因此防止内存泄漏   代码。

怎么会这样?我无法弄清楚它是如何运作的。

1 个答案:

答案 0 :(得分:10)

正常情况下,理查德·伯德(Richard Bird)制定埃拉托斯讷(Eratosthenes)筛子的素数流的定义是自我指导的:

import Data.List.Ordered (minus, union, unionAll)

ps = ((2:) . minus [3..] . foldr (\p r -> p*p : union [p*p+p, p*p+2*p..] r) []) ps

此定义生成的素数ps用作输入。为了防止恶性循环,定义用初始值2引发。这对应于Eratosthenes筛子的数学定义,即在复合材料之间的间隙中找到素数,为每个素数列举 p p P = {2} U ({3,4,... } \ U {{ p 2 p 2 + p 2 + 2p ,...} | P }中 p

生成的流在其自己的定义中用作输入。这导致整个素数流保留在内存中(或者无论如何都保留在内存中)。这里的修复点是共享 corecursive

fix f  = xs where xs = f xs                    -- a sharing fixpoint combinator
ps     = fix ((2:) . minus [3..] . foldr (...) [])
    -- = xs where xs = 2 : minus [3..] (foldr (...) [] xs)

想法 (由于Melissa O'Neill),然后将其分成两个流,内部循环进给进入第二个素数流“上面”:

fix2 f  = f xs where xs = f xs                 -- double-staged fixpoint combinator
ps2     = fix2 ((2:) . minus [3..] . foldr (...) [])
     -- = 2 : minus [3..] (foldr (...) [] xs) where
     --                                   xs = 2 : minus [3..] (foldr (...) [] xs)

因此,当ps2产生一些素数p时,“核心”素数的内部流xs只需要实例化到大约{{1}然后,系统会立即丢弃由sqrt p生成的所有素数并将其垃圾收集:

    \
     \
      <- ps2 <-.
                \
                 \
                  <- xs <-.
                 /         \ 
                 \_________/ 

内循环ps2生成的素数无法立即丢弃,因为xs流本身需要它们。当xs生成素数xs时,只有在q部分计算消耗之后才能丢弃sqrt q以下的部分。换句话说,这个序列将指针保持在自身最低生成值的foldr之内(因为它正由消费者使用,如sqrt)。

因此,使用一个Feed循环(使用print)几乎整个序列都必须保留在内存中,而使用double feed(使用fix)时,只需要保留内部循环只能达到主流产生的当前值的平方根。因此,整体空间复杂度从大约 O(N)减少到大约 O(sqrt(N)) - 大幅减少。

为了实现这一点,代码必须使用优化进行编译,即使用fix2开关,并独立运行。您可能还必须使用-O2开关。并且在测试代码中必须只有一个-fno-cse的引用:

ps2

事实上,在Ideone上进行测试时,it does show实际上是一个不变的内存消耗。


这是Eratosthenes的筛子,而不是Euler的筛子。

最初的定义是:

main = getLine >>= (read >>> (+(-1)) >>> (`drop` ps2) >>> print . take 5)

由于倍数的过早处理,两者效率都很低。通过将eratos (x:xs) = x : eratos (minus xs $ map (*x) [x..] ) -- ps = eratos [2..] eulers (x:xs) = x : eulers (minus xs $ map (*x) (x:xs)) -- ps = eulers [2..] 和枚举融合到一个相距较远的枚举(从mapx,即{{1},可以很容易地修复第一个定义。 }}),因此它的处理可以 推迟 - 因为这里每个素数的倍数都是 独立生成 (以固定间隔列举):

x*x

与本帖顶部的Bird's筛相同,分段

[x*x, x*x+x..]

(此处使用eratos (p:ps) xs | (h,t) <- span (< p*p) xs = -- ps = 2 : eratos ps [2..] h ++ eratos ps (minus t [p*p, p*p+p..]) -- "postponed sieve" 作为简写。)

第二个定义没有简单的解决方法,即ps = 2 : [n | (r:q:_, px) <- (zip . tails . (2:) . map (^2) <*> inits) ps, n <- [r+1..q-1] `minus` foldr union [] [[s+p, s+2*p..q-1] | p <- px, let s = r`div`p*p]]


另外:您可以看到使用Python生成器实现的相同想法,以进行比较here

事实上,Python代码采用了短暂的素数流的伸缩,多级递归生成;在Haskell we can arrange for it中使用非共享,多阶段的固定点组合器 (f <*> g) x = f x (g x)

eulers