Haskell垃圾收集器如何有效地收集树木

时间:2015-12-23 10:37:33

标签: haskell garbage-collection

下面复制的此question答案的代码非常合适,只需O(n)个空间即可对包含n的深度O(2^n)树进行深度优先遍历节点。这非常好,垃圾收集器似乎在清理已经处理过的树上做得很好。

但我的问题是,怎么样?与列表不同,一旦我们处理第一个元素,我们就可以完全忘记它,我们不能在处理第一个叶节点后废弃根节点。我们必须等到树的左半部分被处理(因为最终我们必须从根部向下遍历)。此外,由于根节点指向它下面的节点,依此类推,一直到叶子,这似乎意味着在我们开始之前我们无法收集任何树的前半部分在下半部分(因为所有这些节点仍将从仍然活动的根节点开始引用它们)。幸运的是,情况并非如此,但有人可以解释一下吗?

import Data.List (foldl')

data Tree = Tree Int Tree Tree

tree n = Tree n (tree (2 * n)) (tree (2 * n + 1))
treeOne = tree 1

depthNTree n t = go n t [] where
  go 0 (Tree x _ _) = (x:)
  go n (Tree _ l r) = go (n - 1) l . go (n - 1) r

main = do
  x <- getLine
  print . foldl' (+) 0 . filter (\x -> x `rem` 5 == 0) $ depthNTree (read x) treeOne

3 个答案:

答案 0 :(得分:4)

实际上,当你下左边的子树时,你不会抓住根。

go n (Tree _ l r) = go (n - 1) l . go (n - 1) r

所以根被转为两个组合在一起的thunks。一个持有对左子树的引用,另一个持有对右子树的引用。根节点本身现在是垃圾。

左右子树只是thunks ,因为树是懒惰的,所以它们还没有消耗太多空间。

我们仅评估go n (Tree _ l r)因为我们正在评估depthNTree n tgo n t []。因此,我们立即强制执行两个组成的go调用,我们只是将根变为:

(go (n - 1) l . go (n - 1) r) []
= (go (n - 1) l) ((go (n - 1) r) [])

由于这是懒惰评估,我们首先进行最外层调用,将((go (n - 1) r) [])作为thunk(因此不再生成r)。

递归到go会强制l,因此我们会生成更多内容。但是我们再一次做同样的事情;再次,树节点立即变为垃圾,我们生成两个包含左右子子树的thunk,然后我们只强制左边的子树。

n来电之后,我们将评估go 0 (Tree x _ _) = (x:)。我们已经生成n对thunk,并强制n左边的那些,将正确的那些留在内存中;因为正确的子树是未评估的thunk,它们每个都是恒定的空间,并且它们只有n个,所以总共只有O(n)个空格。并且所有通向此路径的树节点现在都未被引用。

我们实际上有最外层的列表构造函数(以及列表的第一个元素)。强制列表的更多内容将探索正在构建的组合链中的那些正确的子树thunks,但它们永远不会超过n

从技术上讲,您已在全局范围tree 1中绑定了对treeOne的引用,因此实际上您可以保留对您生成的每个节点的引用,因此您依赖GHC注意到treeOne只使用一次,不应保留。

答案 1 :(得分:3)

让我们将go的递归情况重写为

go n t = case t of
           Tree _ l r -> go (n - 1) l . go (n - 1) r

在案例替代方案的右侧,原始树t不再存在。只有lr有效。所以,如果我们首先递归l,那么除了l本身之外,没有任何东西可以保持树的左侧。 r确实使树的右侧保持活着。

在递归的任何一点,实时节点正好是从树的原始根到当前正在检查的节点(尚未处理的)的路径所截断的子树的根。这些子树的所述路径的长度最多,因此空间使用量为O(n)。

关键是原始树t在我们递归之前就已经死了。如果你写出(由于多种原因而在表意上相同但风格不好)

leftChild (Tree _ l r) = l
rightChild (Tree _ l r) = r

go n t = go (n - 1) (leftChild t) . go (n - 1) (rightChild t)

现在,在递归到go (n - 1) (leftChild t)时,在未评估的表达式t中仍然存在对rightChild t的实时引用。因此,空间使用现在是指数级的。

答案 2 :(得分:3)

我写了一个深度为2的树的手工评估。我希望它可以说明为什么树节点可以在路上被垃圾收集。

假设我们从这样的树开始:

tree =
  Tree
    (Tree _          -- l
       (Tree a _ _)    -- ll
       (Tree b _ _))   -- lr
    (Tree _          -- r
       (Tree c _ _)    -- rl
       (Tree d _ _))   -- rr

现在致电depthNTree 2 tree

go 2 tree []
go 2 (Tree _ l r) []            
go 1 l (go 1 r [])
go 1 (Tree _ ll lr) (go 1 r [])
go 0 ll (go 0 lr (go 1 r []))
go 0 (Tree a _ _) (go 0 lr (go 1 r []))
a : go 0 lr (go 1 r [])                   -- gc can collect ll
a : go 0 (Tree b _ _) (go 1 r [])
a : b : go 1 r []                         -- gc can collect lr and thus l
a : b : go 1 (Tree _ rl rr) []
a : b : go 0 rl (go 0 rr [])
a : b : go 0 (Tree c _ _) (go 0 rr [])
a : b : c : go 0 rr []                    -- gc can collect rl
a : b : c : go 0 (Tree d _ _) []
a : b : c : d : []                        -- gc can collect rr and thus r and tree

请注意,由于treeOne是一个静态值,因此必须在幕后添加一些额外的机制以允许垃圾收集。幸运的是GHC supports GC的静态值。