懒惰的树与空间泄漏

时间:2011-07-09 23:17:54

标签: haskell lazy-evaluation

我正在编写一个试图实现玩具XML处理器的程序。现在,该程序应该读取描述文档结构的事件流(想想SAX),并懒惰地构建相应的树。

事件由以下数据类型定义:

data Event = Open String
           | Close

可能的输入是:

[Open "a", Open "b", Close, Open "c", Close, Close]

与树对应:

  a
 / \
b   c

我想以懒惰的方式生成树,因此它不需要在任何时候以完整形式存在于内存中。但是,我当前的实现似乎存在空间泄漏,导致所有节点即使在不再需要时也会被保留。这是代码:

data Event = Open String
           | Close

data Tree a = Tree a (Trees a)

type Trees a = [Tree a]

data Node = Node String


trees [] = []
trees (Open x : es) =
    let (children, rest) = splitStream es
    in  (Tree (Node x) (trees children)) : (trees rest)

splitStream es = scan 1 es

scan depth (s@(Open {}) : ss) =
    let (b, a) = scan (depth+1) ss
    in  (s:b, a)
scan depth (s@Close : ss) =
    case depth of
      1 -> ([], ss)
      x -> let (b, a) = scan (depth-1) ss
           in  (s:b, a)


getChildren = concatMap loop
  where
    loop (Tree _ cs) = cs


main = print .
       length .
       getChildren .
       trees $
       [Open "a"] ++ (concat . replicate 1000000 $ [Open "b", Close]) ++ [Close]

函数trees将事件列表转换为Tree Node列表。 getChildren收集根(“a”)的所有子节点(标记为“b”)。然后对它们进行计数,并打印出结果数字。

使用GHC 7.0.4(-O2)构建的编译程序不断增加其内存使用量,直至打印节点数。另一方面,我期待几乎恒定的内存使用。

查看“-hd”堆配置文件,很明显大部分内存都是由列表构造函数(:)获取的。似乎scantrees生成的其中一个列表已完整保留。但是,我不明白为什么length . getChildren应该在遍历子节点时立即删除它们。

有没有办法解决这种空间泄漏?

2 个答案:

答案 0 :(得分:6)

我怀疑trees是邪恶的家伙。正如John L所说,这可能是Wadler Space Leak的一个实例,其中编译器无法应用防止泄漏的优化。问题是您使用延迟模式匹配(let表达式)来解构对,并通过在元组的一个组件上应用trees来执行模式匹配。 http://comments.gmane.org/gmane.comp.lang.haskell.glasgow.user/19129曾经有一个非常类似的问题。该线程还提供了更详细的解释。为了防止空间泄漏,您只需使用case表达式来解构元组,如下所示。

trees [] = []
trees (Open x : es) =
  case splitStream es of
       (children, rest) -> Tree (Node x) (trees children) : trees rest

通过此实现,最大驻留时间从38MB降至28KB。

但请注意,trees的这一新实现比原始实现更严格,因为它要求splitStream的应用。因此,在某些情况下,这种转变甚至可能导致空间泄漏。要重新获得不太严格的实现,您可能会使用与Data.List中的lines函数类似的技巧,这会导致类似的问题http://hackage.haskell.org/packages/archive/base/latest/doc/html/src/Data-List.html#lines。在这种情况下,trees看起来如下。

trees [] = []
trees (Open x : es) =
  context (case splitStream es of
                (children, rest) -> (trees children, trees rest))
 where
   context ~(children', rest') = Tree (Node x) children' : rest'

如果我们考虑延迟模式匹配,我们得到以下实现。这里编译器能够检测元组组件的选择器,因为我们不在其中一个组件上执行模式匹配。

trees [] = []
trees (Open x : es) = Tree (Node x) children' : rest'
 where
  (children', rest') =
     case splitStream es of
          (children, rest) -> (trees children, trees rest)

有人知道这种转变是否总能解决问题吗?

答案 1 :(得分:5)

我强烈怀疑这是"Wadler space leak"错误的一个例子。不幸的是我不知道如何解决它,但我确实发现了一些可以减轻影响的事情:

1)将getChildren更改为

getChildren' = ($ []) . foldl (\ xsf (Tree _ cs) -> xsf . (cs ++)) id

这是一个很小但很明显的改进。

2)在此示例中,trees始终输出单个元素列表。如果对于您的数据始终如此,则显式删除列表的其余部分会修复空间泄漏:

main = print .
   length .
   getChildren .
   (:[]) .
   head .
   trees