Ленивое дерево с утечкой пространства
Я пишу программу, пытающуюся реализовать игрушечный 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 ([email protected](Open {}) : ss) =
let (b, a) = scan (depth+1) ss
in (s:b, a)
scan depth ([email protected] : 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
собирает все дочерние узлы (помеченные как "b" ) корня ( "a" ). Затем они подсчитываются и выводится результирующее число.
Скомпилированная программа, построенная с помощью GHC 7.0.4 (-O2), продолжает увеличивать объем использования памяти до момента, когда печатает счетчик node. С другой стороны, я ожидал почти постоянного использования памяти.
Глядя на профиль "-hd" кучи, ясно, что большую часть памяти занимает конструктор списка (:
). Кажется, что один из списков, созданных scan
или trees
, сохраняется полностью. Однако я не понимаю, почему, поскольку length . getChildren
должен избавиться от дочерних узлов, как только они пройдут.
Есть ли способ исправить такую утечку пространства?
Ответы
Ответ 1
Я подозреваю, что trees
- злой парень. Поскольку Джон Л сказал, что это, вероятно, экземпляр 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
При такой реализации максимальное место жительства уменьшается от 38 МБ до 28 КБ.
Но обратите внимание, что эта новая реализация trees
более строгая, чем оригинальная, поскольку она требует применения splitStream
. Поэтому в некоторых случаях это преобразование может даже вызвать утечку пространства. Чтобы восстановить менее строгую реализацию, вы можете использовать подобный трюк, как функция lines
в Data.List, которая вызывает аналогичную проблему 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)
Кто-нибудь знает, всегда ли это преобразование делает трюк?
Ответ 2
Я сильно подозреваю, что это пример ошибки "Wadler space leak" . К сожалению, я не знаю, как это решить, но я нашел несколько вещей, которые немного смягчают эффекты:
1) Измените getChildren
на
getChildren' = ($ []) . foldl (\ xsf (Tree _ cs) -> xsf . (cs ++)) id
Это небольшое, но заметное улучшение.
2) В этом примере trees
всегда выводит список из одного элемента. Если это всегда верно для ваших данных, явное удаление остальной части списка устраняет утечку пространства:
main = print .
length .
getChildren .
(:[]) .
head .
trees