当您的算法依赖于懒惰时,如何修复由懒惰引起的空间泄漏

时间:2015-10-16 23:38:18

标签: performance haskell

我有一些生成搜索树的算法:

data SearchTree a = Solution a | Contradiction | Search [ SearchTree a ]
  deriving (Show, Functor)

算法懒惰地生成这棵树。我还定义了一个简单的求值器,它实际上只是深度优先搜索。

simpleEval :: MonadPlus m => SearchTree a -> m a
simpleEval (Solution a) = return a 
simpleEval Contradiction = mzero 
simpleEval (Search ps) = foldr mplus mzero $ map simpleEval ps 

我注意到我的算法生成的许多解决方案类似于以下搜索树:

nest :: Int -> SearchTree a -> SearchTree a 
nest 0 = id 
nest n = nest (n-1) . Search . (:[]) 

tree0 = Search ts where 
  ts = cycle $ t0 : replicate 100 t1 ++ [t2]
  t0 = nest 100 $ Solution 'a' 
  t1 = nest 1000 $ Contradiction 
  t2 = nest 4 $ Solution 'b' 

即,它们有很多非常深的分支,没有解决方案,一些深层分支有一个解决方案,很少有浅层分支和一个解决方案。在此基础上,我决定我想要另一个评估员,一个将放弃的评估员。在太深的树枝上。称之为cutoffEvalcutoffEval 5 tree0应该只找到b因为它有无限多个深度小于5的分支要考虑,它们只包含b s。我这样实现了它:

cutoff :: (MonadPlus m) => Int -> SearchTree a -> (m a, [SearchTree a])
cutoff cu = go cu where 
  plus ~(m0, l0) ~(m1, l1) = (mplus m0 m1, l0 ++ l1)
  zero = (mzero, [])

  go 0 x             = (mzero, [x])
  go _ Contradiction = zero
  go _ (Solution a)  = (return a, [])
  go d (Search ps)   = foldr plus zero $ map (go $ d-1) ps

cutoffEval :: MonadPlus m => Int -> SearchTree a -> m a
cutoffEval cu = go where 
  go t = case cutoff cu t of (r,ts) -> foldr mplus mzero $ r : map go ts 

但与simpleEval

相比,此函数会产生巨大的空间泄漏
putStrLn $ take 4000 $ simpleEval tree0  -- 2MB residency
putStrLn $ take 4000 $ cutoffEval 10 tree0  -- 600MB residency

分析显示几乎所有分配都发生在cutoff.go;并且大多数分配是由于一些神秘的,称为main:Tree.sat_s5jg(,)构造函数。在我看来,由于无可辩驳的模式,元组构造函数被构建为thunk而不是被plus强制;并且通常空间泄漏的解决方案是使您的函数更严格,但是在这里删除无可辩驳的模式导致cutoff挂起,所以我不能这样做。

我用GHC 7.6,7.8和7.10测试了这个。每个人都发现了这个问题。

所以我的问题是:cutoffEval可以编写为在simpleEval这样的常量空间中运行吗?更一般地说,如果我不能使我的实现更严格,因为算法取决于它,我如何修复空间泄漏?

1 个答案:

答案 0 :(得分:1)

在我看来,内存泄漏的原因实际上是一个错误。你的cutoff函数混合在一起,切断过深的分支,评估上半部分。然后在cutoffEval中,你会深入到底部,切割树枝,并继续以递归方式探索它们。这实际上是广度优先搜索,每次传递的cu级别。这意味着整个树最终将构建并保留在内存中直到结束! (与深度优先搜索的情况不同,GC可以回收访问的子树。)

如果你想切断太深的树枝,那么得到cutoff结果的第一部分就是你想要的。

无论如何,我建议将评估者和截止部分分开(见下文)。在这种情况下,您可以在树的截止版本上使用原始评估程序。

另外一条评论来自MonadPlus约束,您只使用幺半部分 - mzeromplus。仅使用Monoid会更清晰,更通用。 monad比monad更多(例如Sum只计算solutoins,或Last找到最后的解决方案。)

simpleEval :: (Monoid m) => (a -> m) -> SearchTree a -> m
simpleEval f = go
  where
    go (Solution a) = f a
    go Contradiction = mempty
    go (Search ps) = mconcat $ map go ps 

cutoff :: Int -> SearchTree a -> SearchTree a
cutoff cu = go cu
  where 
    go 0 _             = Contradiction -- too deep branches are just failures
    go d (Search ps)   = Search $ map (go (d - 1)) ps
    go _ x             = x

cutoffEval :: (Monoid m) => Int -> (a -> m) -> SearchTree a -> m
cutoffEval cu f = simpleEval f . cutoff cu