Почему обертка монады Data.Binary.Put создает утечку памяти? (Часть 2) - PullRequest
6 голосов
/ 16 февраля 2011

Как и в моем предыдущем вопросе , я пытаюсь заключить монаду Data.Binary.Put в другую монаду, чтобы позже я мог задать ей такие вопросы, как «сколько байт она собирается записать» или msgstr "какая текущая позиция в файле".

Раньше я думал, что понимание того, почему происходит утечка памяти при использовании тривиальной оболочки (IdentityT?), Приведет меня к решению моей проблемы. Но даже при том, что вы, ребята, помогли мне решить проблему с помощью тривиальной оболочки, обертывание ее чем-нибудь полезным, например, StateT или WriterT, все равно потребляет слишком много памяти (и обычно вылетает).

Например, это один из способов, который я пытаюсь обернуть, и который приводит к утечке памяти при большом вводе:

type Out = StateT Integer P.PutM ()

writeToFile :: String -> Out -> IO ()
writeToFile path out = BL.writeFile path $ P.runPut $ do runStateT out 0
                                                         return ()

Здесь - более полный пример кода, демонстрирующий проблему.

То, что я хотел бы знать, это:

  1. Что происходит внутри программы, что вызывает утечку памяти?
  2. Что я могу сделать, чтобы это исправить?

Что касается моего второго вопроса, я думаю, что должен объяснить более подробно, что я намерен просматривать данные на диске: это в основном древовидная структура, где каждый узел дерева представлен в виде таблицы смещений для его дочерних элементов (плюс некоторые дополнительные данные). Таким образом, чтобы вычислить смещение n-ых дочерних элементов в таблицу смещений, мне нужно знать размеры дочерних элементов от 0 до n-1 плюс текущее смещение (скажем, для упрощения, скажем, каждый узел имеет фиксированное число дочерних элементов).

Спасибо за внимание.

UPDATE: Благодаря nominolo я теперь могу создать монаду, которая оборачивается вокруг Data.Binary.Put, отслеживает текущее смещение и почти не использует память. Это делается путем отказа от использования преобразователя StateT в пользу другого механизма потоков состояния, который использует Continuations.

Вот так:

type Offset = Int

newtype MyPut a = MyPut
  { unS :: forall r . (Offset -> a -> P.PutM r) -> Offset -> P.PutM r }

instance Monad MyPut where
  return a = MyPut $ \f s -> f s a
  ma >>= f = MyPut $ \fb s -> unS ma (\s' a -> unS (f a) fb s') s

writeToFile :: String -> MyPut () -> IO ()
writeToFile path put =
  BL.writeFile path $ P.runPut $ peal put >> return ()
  where peal myput = unS myput (\o -> return) 0

getCurrentOffset :: MyPut Int
getCurrentOffset = MyPut $ \f o -> f o o

lift' n ma = MyPut $ \f s -> ma >>= f (s+n)

Однако у меня все еще есть проблема с отслеживанием количества байтов, которые MyPut собирается записать на диск. В частности, мне нужно иметь функцию с такой подписью:

getSize :: MyPut a -> MyPut Int
или
getSize :: MyPut a -> Int

Мой подход заключался в том, чтобы обернуть монаду MyPut внутри преобразователя WriterT (что-то вроде this ). Но это снова стало занимать слишком много памяти. Как sclv упоминает в комментариях под nominolos ответом, WriterT каким-то образом нейтрализует эффект продолжений. Он также упоминает, что получение размера должно быть возможным непосредственно из монады MyPut, которая у меня уже есть, но все мои попытки сделать это закончились некомпилируемым кодом или бесконечным циклом: - |.

Может кто-нибудь помочь, пожалуйста?

Ответы [ 2 ]

8 голосов
/ 16 февраля 2011

Похоже, монадный трансформатор слишком ленив.Вы можете создать профиль кучи (не создавая его специально), запустив программу с:

$ ./myprog +RTS -hT
$ hp2ps myprog.hp
$ open hp2ps.ps    # Or whichever viewer you have

В этом случае это не особенно полезно, потому что показывает только PAP s, FUN_1_0 s и FUN_2_0 s.Это означает, что куча состоит из множества частично примененных функций и функций одного аргумента и двух аргументов.Обычно это означает, что что-то недостаточно оценено.Монадные трансформаторы несколько печально известны этим.

Обходной путь должен использовать более строгие монадные трансформаторы, использующие стиль передачи продолжения .(его требует {-# LANGUAGE Rank2Types #-}.

newtype MyStateT s m a =
  MyStateT { unMyStateT :: forall r. (s -> a -> m r) -> s -> m r }

Стиль передачи продолжения означает, что вместо прямого возврата результата мы вызываем другую функцию, продолжение , с нашим результатом, в данном случае s и a. Определения экземпляров выглядят немного забавно. Чтобы понять это, прочитайте ссылку выше (Википедия).

instance Monad m => Monad (MyStateT s m) where
  return x = MyStateT (\k s -> k s x)
  MyStateT f >>= kk = MyStateT (\k s ->
    f (\s' a -> unMyStateT (kk a) k s') s)

runMyStateT :: Monad m => MyStateT s m a -> s -> m (a, s)
runMyStateT (MyStateT f) s0 = f (\s a -> return (a, s)) s0

instance MonadTrans (MyStateT s) where
  lift act = MyStateT (\k s -> do a <- act; k s a)

type Out = MyStateT Integer P.PutM ()

Запуск этого теперь дает постоянный пробел (бит "максимальная резидентность"):

$ ./so1 +RTS -s 
begin
end
   8,001,343,308 bytes allocated in the heap
     877,696,096 bytes copied during GC
          46,628 bytes maximum residency (861 sample(s))
          33,196 bytes maximum slop
            2 MB total memory in use (0 MB lost due to fragmentation)

Generation 0: 14345 collections,     0 parallel,  3.32s,  3.38s elapsed
Generation 1:   861 collections,     0 parallel,  0.08s,  0.08s elapsed

Недостатком использования таких строгих преобразователей является то, что вы больше не можете определять MonadFix экземпляров, а некоторые трюки лени больше не работают.

2 голосов
/ 02 марта 2011

Я начал играть с этим и понял, в чем заключается большая проблема - ваш алгоритм ужасно сложен.Вместо того, чтобы вычислять размер каждого дочернего дерева один раз, вы вычисляете его один раз для каждого вызова getSize.И вы вызываете getSize рекурсивно.Для каждого конечного узла getSize вызывается один раз для каждого вызова getSize его родителя.И getSize вызывается для каждого родителя один раз для себя + один раз для каждого вызова getSize для любого из его родителей.Так что getize называется, по крайней мере, геометрически в глубине дерева.Вам необходимо кэшировать размеры, чтобы получить что-то, похожее на разумное время выполнения.

Тем не менее, вот версия основных функций, которая, кажется, работает правильно без утечки, хотя она действительно ползет по причинам, указанным выше:

type MyPut = S (Offset,Size) P.PutM

peal_1 :: (Monad m, Num t, Num t1) => S (t, t1) m a -> m a
peal_1 put = unS put (\o -> return) (0,0)

writeToFile :: String -> MyPut () -> IO ()
writeToFile path put =
  BL.writeFile path $ P.runPut $ (peal_1 put) >> return ()

getSize :: MyPut a -> MyPut Int
getSize x = S $ \f os -> unS (x >> getCurrentSize) f os

getCurrentOffset :: MyPut Int
getCurrentOffset = S $ \f os -> f os (fst os)

getCurrentSize :: MyPut Int
getCurrentSize = S $ \f os -> f os (snd os)

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

Но для ваших больших тестовых данных этот код написал 6,5 ГБ, прежде чем я его убил(предоставленный код исчерпал кучу задолго до этого).Я подозреваю, но не проверял, что базовые вызовы в монаде put запускаются один раз для каждого вызова getSize, даже если результат getSize отбрасывается.

Мое предлагаемое правильное решение опубликовано в качестве ответана ваш другой вопрос: Как сохранить древовидную структуру данных в двоичном файле в Haskell

...