Как сохранить древовидную структуру данных в двоичный файл в Haskell

I'm trying to save a simple (but quite big) Tree structure into a binary file using Haskell. The structure looks something like this:

-- For simplicity assume each Node has only 4 childs
data Tree = Node [Tree] | Leaf [Int]
And here is how I need the data look on disk:

  1. Каждый узел начинается с четырех 32-битных смещений к своим дочерним элементам, а затем следует за дочерними элементами.
  2. Меня не волнуют листья, скажем, это просто n последовательных 32-битных чисел.
  3. Для практических целей мне понадобятся некоторые метки узлов или некоторые другие дополнительные данные, но сейчас меня это тоже не волнует.

Мне кажется, что первым выбором Haskeller при написании двоичных файлов является библиотека Data.Binary.Put. А вот с этим у меня проблема в пуле №1. В частности, когда я собираюсь записать узел в файл, чтобы записать дочерние смещения, мне нужно знать свое текущее смещение и размер каждого дочернего элемента.

Это не то, что предоставляет Data.Binary.Put, поэтому я подумал, что это должно быть идеальное применение преобразователей Monad. Но хоть это звучит круто и функционально, пока у меня не получилось с таким подходом.

Я задал два других вопроса, которые, как я думал, помогут мне решить проблему -leak">здесь и здесь. Я должен сказать, что каждый раз я получал очень хорошие ответы, которые помогали мне двигаться дальше, но, к сожалению, я все еще не могу решить проблему в целом.

Вот то, что у меня есть до сих пор, это все еще слишком много памяти, чтобы быть практичным.

Я хотел бы иметь решение, использующее такой функциональный подход, но был бы признателен и за любое другое решение.


person Peter Jankuliak    schedule 01.03.2011    source источник
comment
Насколько велико дерево и какой размер файла вы бы себе представляли? Ответ на этот вопрос определяет, можете ли вы вообще использовать какую-либо структуру типа put или вам нужно что-то, что включает однопроходный обход, но модифицирует уже написанные части вашей структуры...   -  person sclv    schedule 01.03.2011
comment
Двоичная сериализация обычно должна знать размер записываемых данных (например, списки имеют префикс длины). Не могли бы вы жить с сериализацией текста (возможно, файлы большего размера)? В противном случае вы можете сделать некоторые трюки, записывая промежуточные файлы и сшивая их вместе (ужасно, но возможно). Кроме того, в вашем тестовом коде ввод является синтетическим - если ваши реальные данные не являются синтетическими, вы все равно можете иметь их в памяти, поэтому обычная двоичная сериализация не будет заставлять ничего, чего еще нет в куче.   -  person stephen tetley    schedule 01.03.2011
comment
@sclv, ссылка, которую я получил выше, указывает на отрывок из более крупной программы, над которой я работаю уже некоторое время. В исходной программе я читаю двоичный файл с похожей структурой, преобразовываю его (в основном, чтобы не было слишком много дочерних элементов на узел), а затем хочу сохранить его обратно. Исходные файлы имеют размер от 50 МБ до 200 МБ, поэтому я полагаю, что конечные файлы будут примерно такого же размера.   -  person Peter Jankuliak    schedule 02.03.2011
comment
@stephen tetley, к сожалению, формат должен оставаться таким, какой он есть (к нему есть некоторые требования, которые обеспечивают соблюдение этой структуры). У меня около 4 ГБ памяти на машине для разработки, и я был бы не против потратить ее на данные, но я думаю, что есть что-то за пределами моего понимания, что занимает память гораздо больше, чем необходимо.   -  person Peter Jankuliak    schedule 02.03.2011
comment
Дерево уже существует в памяти? Или он лениво вычисляется по запросу? Если второе, то, возможно, ваша утечка — это создание всего дерева.   -  person Paul Johnson    schedule 02.03.2011
comment
@Paul: я думаю, что создание всего дерева не должно быть проблемой, если мои расчеты верны, дерево в приведенной выше ссылке примера должно занимать приблизительно 101 * 4^9 * sizeof(Int) + 1/3 * 4 ^9 * sizeof(Node) =~ 110 МБ при условии, что sizeof(Int) = 4 и sizeof(Node) = 16. Даже если были некоторые накладные расходы, вызванные, например. GC он все еще должен легко помещаться в моей оперативной памяти (я думаю).   -  person Peter Jankuliak    schedule 02.03.2011
comment
Если дерево большое, то его сериализация также создаст большой буфер строки байтов в куче - Data.Binary.Put не может постепенно записывать в файл. То, что у вас есть, является сложной проблемой, поскольку деревья не передаются естественным образом, поэтому (наивному) алгоритму потребуется все дерево в памяти. Без сомнения, существует классическое алгоритмическое решение для повторной балансировки дерева без загрузки его всего в память, но, боюсь, я его не знаю.   -  person stephen tetley    schedule 02.03.2011
comment
Для списка целых чисел требуется 8 или 12 байтов на элемент.   -  person Paul Johnson    schedule 02.03.2011


Ответы (4)


Вот реализация двухпроходного решения, предложенного sclv.

import qualified Data.ByteString.Lazy as L
import Data.Binary.Put
import Data.Word
import Data.List (foldl')

data Tree = Node [Tree] | Leaf [Word32] deriving Show

makeTree 0 = Leaf $ replicate 100 0xdeadbeef
makeTree n = Node $ replicate 4 $ makeTree $ n-1

SizeTree имитирует исходное дерево, оно не содержит данных, но в каждом узле хранит размер соответствующего дочернего элемента в дереве.
Нам нужно иметь SizeTree в памяти, поэтому стоит сделать его более компактным (например, заменить Ints словами в ubox) .

data SizeTree
  = SNode {sz :: Int, chld :: [SizeTree]}
  | SLeaf {sz :: Int}
  deriving Show

Имея SizeTree в памяти, можно сериализовать оригинальное дерево в потоковом режиме.

putTree :: Tree -> SizeTree -> Put
putTree (Node xs) (SNode _ ys) = do
  putWord8 $ fromIntegral $ length xs          -- number of children
  mapM_ (putWord32be . fromIntegral . sz) ys   -- sizes of children
  sequence_ [putTree x y | (x,y) <- zip xs ys] -- children data
putTree (Leaf xs) _ = do
  putWord8 0                                   -- zero means 'leaf'
  putWord32be $ fromIntegral $ length xs       -- data length
  mapM_ putWord32be xs                         -- leaf data


mkSizeTree :: Tree -> SizeTree
mkSizeTree (Leaf xs) = SLeaf (1 + 4 + 4 * length xs)
mkSizeTree (Node xs) = SNode (1 + 4 * length xs + sum' (map sz ys)) ys
  where
    ys = map mkSizeTree xs
    sum' = foldl' (+) 0

Важно предотвратить объединение GHC двух проходов в один (в этом случае дерево будет храниться в памяти). Здесь это делается путем подачи в функцию не дерева, а генератора дерева.

serialize mkTree size = runPut $ putTree (mkTree size) treeSize
  where
    treeSize = mkSizeTree $ mkTree size

main = L.writeFile "dump.bin" $ serialize makeTree 10
person max taldykin    schedule 06.03.2011

Есть два основных подхода, которые я бы рассмотрел. Если вся сериализованная структура легко поместится в памяти, вы можете сериализовать каждый узел в ленивую строку байтов и просто использовать длину каждого из них для вычисления смещения от текущей позиции.

serializeTree (Leaf nums)  = runPut (mapM_ putInt32 nums)
serializeTree (Node subtrees) = mconcat $ header : childBs
 where
  childBs = map serializeTree subtrees
  offsets = scanl (\acc bs -> acc+L.length bs) (fromIntegral $ 2*length subtrees) childBs
  header = runPut (mapM_ putInt32 $ init offsets)

Другой вариант — после сериализации узла вернуться назад и переписать поля смещения соответствующими данными. Это может быть единственный вариант, если дерево большое, но я не знаю библиотеки сериализации, поддерживающей это. Это потребует работы в IO и seek в правильных местах.

person John L    schedule 01.03.2011

Я думаю, вам нужно явное двухпроходное решение. Первый преобразует ваше дерево в дерево с аннотациями размера. Этот проход форсирует дерево, но на самом деле его можно выполнить вообще без какой-либо монадической машины, просто завязав узел. Второй проход находится в старой доброй монаде Put, и, учитывая, что аннотации размера уже рассчитаны, он должен быть очень простым.

person sclv    schedule 02.03.2011
comment
это сработает, однако я думаю, что мое решение будет более эффективным с точки зрения памяти. Это связано с тем, что мой подход позволяет собирать каждое поддерево после его сериализации, а сериализация строки байтов должна быть намного меньше, чем фактическое дерево. - person John L; 03.03.2011
comment
@Джон, наверное, ты прав. Ваше решение действительно однопроходное, но не полностью потоковое. - person sclv; 03.03.2011

Вот реализация с использованием Builder, который является частью «бинарного» пакета. Я не профилировал его должным образом, но, согласно «сверху», он сразу выделяет 108 Мбайт, а затем зависает на этом до конца выполнения.

Обратите внимание, что я не пытался считывать данные обратно, поэтому в моих расчетах размера и смещения могут быть скрытые ошибки.

-- Paste this into TreeBinary.hs, and compile with
--    ghc -O2 --make TreeBinary.hs -o TreeBinary

module Main where


import qualified Data.ByteString.Lazy as BL
import qualified Data.Binary.Builder as B

import Data.List (init)
import Data.Monoid
import Data.Word


-- -------------------------------------------------------------------
-- Test data.

data Tree = Node [Tree] | Leaf [Word32] deriving Show

-- Approximate size in memory (ignoring laziness) I think is:
-- 101 * 4^9 * sizeof(Int) + 1/3 * 4^9 * sizeof(Node)

-- This version uses [Word32] instead of [Int] to avoid having to write
-- a builder for Int.  This is an example of lazy programming instead
-- of lazy evaluation. 

makeTree :: Tree
makeTree = makeTree1 9
  where makeTree1 0 = Leaf [0..100]
        makeTree1 n = Node [ makeTree1 $ n - 1
                           , makeTree1 $ n - 1
                           , makeTree1 $ n - 1
                           , makeTree1 $ n - 1 ]

-- --------------------------------------------------------------------
-- The actual serialisation code.


-- | Given a tree, return a builder for it and its estimated length in bytes.
serialiseTree :: Tree -> (B.Builder, Word32)
serialiseTree (Leaf ns) = (mconcat (B.singleton 2 : map B.putWord32be ns), fromIntegral $ 4 * length ns + 1)
serialiseTree (Node ts) = (mconcat (B.singleton 1 : map B.putWord32be offsets ++ branches), 
                           baseLength + sum subLengths)
   where
      (branches, subLengths) = unzip $ map serialiseTree ts
      baseLength = fromIntegral $ 1 + 4 * length ts
      offsets = init $ scanl (+) baseLength subLengths


main = do
   putStrLn $ "Length = " ++ show (snd $ serialiseTree makeTree)
   BL.writeFile "test.bin" $ B.toLazyByteString $ fst $ serialiseTree makeTree
person Paul Johnson    schedule 06.03.2011