Эффективная регистрация строковых данных в ST Monad на Haskell

У меня есть программа на Haskell, которая генерирует ~280M записей текстовых данных во время прогона внутри монады ST. Именно здесь происходит практически все потребление памяти (при отключенном ведении журнала программа выделяет в общей сложности 3 МБ реальной памяти).

Проблема в том, что у меня не хватает памяти. В то время как программа работает, потребление памяти превышает 1,5 ГБ, и, наконец, она заканчивается, когда она пытается записать строку журнала в файл.

Функция log принимает строку и накапливает данные журнала в компоновщик строк, хранящийся в STRef в среде:

import qualified Data.ByteString.Lazy.Builder as BB
...
myLogFunction s = do
    ...
    lift $ modifySTRef myStringBuilderRef (<> BB.stringUtf8 s)

Я пытался ввести строгость, используя шаблоны взрыва и modifySTRef', но это сделало потребление памяти еще хуже.

Я пишу строку журнала в соответствии с рекомендациями документации hPutBuilder, например:

    hSetBinaryMode h True
    hSetBuffering  h $ BlockBuffering Nothing
    BB.hPutBuilder h trace

Это потребляет несколько дополнительных ГБ памяти. Я пробовал разные настройки буферизации и сначала конвертировать в ленивую строку ByteString (чуть лучше).

Qs:

  • Как я могу минимизировать потребление памяти во время работы программы? Я ожидал бы, что при строгом представлении ByteString и соответствующей степени строгости мне понадобится немного больше памяти, чем ~280M фактических данных журнала, которые я храню.

  • Как я могу записать результат в файл без выделения памяти? Я не понимаю, почему Haskell требуется ГБ памяти, чтобы просто передавать некоторые резидентные данные в файл.

Редактировать:

Вот профиль памяти для небольшого прогона (~42 МБ данных журнала). Общее использование памяти составляет 3 МБ с отключенным ведением журнала.

    15,632,058,700 bytes allocated in the heap
     4,168,127,708 bytes copied during GC
       343,530,916 bytes maximum residency (42 sample(s))
         7,149,352 bytes maximum slop
               931 MB total memory in use (0 MB lost due to fragmentation)

                                      Tot time (elapsed)  Avg pause  Max pause
    Gen  0     29975 colls,     0 par    5.96s    6.15s     0.0002s    0.0104s
    Gen  1        42 colls,     0 par    6.01s    7.16s     0.1705s    1.5604s

    TASKS: 3 (1 bound, 2 peak workers (2 total), using -N1)

    SPARKS: 0 (0 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)

    INIT    time    0.00s  (  0.00s elapsed)
    MUT     time   32.38s  ( 33.87s elapsed)
    GC      time   11.97s  ( 13.31s elapsed)
    RP      time    0.00s  (  0.00s elapsed)
    PROF    time    0.00s  (  0.00s elapsed)
    EXIT    time    0.00s  (  0.00s elapsed)
    Total   time   44.35s  ( 47.18s elapsed)

    Alloc rate    482,749,347 bytes per MUT second

    Productivity  73.0% of total user, 68.6% of total elapsed

Редактировать:

Я запустил профиль памяти с небольшим журналом, запрошенным:

http://imageshack.us/a/img14/9778/6a5o.png

Я пытался добавить шаблоны взрыва, $!, deepseq/$!!, force и тому подобное в соответствующих местах, но, похоже, это не имеет никакого значения. Как заставить Haskell фактически взять мое выражение string / printf и т. Д. И поместить его в плотный ByteString вместо того, чтобы хранить все эти списки [Char] и неоцененные блоки?

Редактировать:

Вот фактическая функция полной трассировки

trace s = do
     enable <- asks envTraceEnable
     when (enable) $ do
        envtrace <- asks envTrace
        let b = B8.pack s
        lift $ b `seq` modifySTRef' envtrace (<> BB.byteString b)

Достаточно ли этого "строгого"? Нужно ли следить за чем-либо, если я вызову эту функцию класса типов внутри моей монады ReaderT/ST? Просто чтобы оно на самом деле вызывалось и никак не откладывалось.

do
    trace $ printf "%i" myint

Это хорошо?

Спасибо!

1 ответ

Решение

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

Но выход есть: используйте какой-нибудь моно-преобразователь сопрограммы, такой как в пакете "pipe". Вот пример с использованием pipe-3.3.0:

{-# LANGUAGE ExplicitForAll #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE LiberalTypeSynonyms #-}

import Control.Monad
import Control.Monad.ST
import Control.Monad.ST (stToIO) -- Transforms ST computations into IO computations
import Control.Monad.Trans
import Control.Monad.Morph (hoist) -- Changes the base monad of a monad transformer
import Control.Proxy.Prelude (stdoutD) -- Consumer that prints to stdout
import Control.Proxy.Core
import Control.Proxy.Core.Correct

import Data.STRef

simpleST :: ST s Bool
simpleST= do
    ref <- newSTRef True
    writeSTRef ref False
    readSTRef ref

-- Like simpleST, but emits log messages during the computation
loggingST :: Producer ProxyCorrect String (ST s) Bool
loggingST = do
    ref <- lift $ newSTRef True
    respond "Before writing"
    lift $ writeSTRef ref False
    respond "After writing"
    lift $ readSTRef ref

adapt :: (forall s . Producer ProxyCorrect String (ST s) a) ->
         Producer ProxyCorrect String IO a
adapt x = hoist stToIO x

main :: IO ()
main = do
    result <- runProxy $ (\_ -> adapt loggingST) >-> stdoutD
    putStrLn . show $ result

Он печатает журнал на стандартный вывод. При запуске выдает следующее:

Before writing
After writing
False

Это работает следующим образом: вы отправляете сообщения журнала в производителя, используя respond в то время как все еще проживает в монаде ST. Таким образом, вы можете войти в систему и при этом быть уверены, что ваши вычисления не выполняют каких-то странных операций ввода-вывода. Тем не менее, он заставляет вас перелистывать свой код лифтами.

После того, как вы построите свои вычисления ST, вы преобразуете базовую монаду производителя из ST в IO, используя hoist, Подъем - это полезная функция, которая позволяет вам менять скатерть, пока блюда находятся на столе.

Теперь мы в IO-земле! Осталось только подключить производителя к потребителю, который на самом деле пишет сообщения (здесь они печатаются на стандартный вывод, но вы также можете легко подключиться к потребителю, который пишет в файл).

Другие вопросы по тегам