Эффективная регистрация строковых данных в 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-земле! Осталось только подключить производителя к потребителю, который на самом деле пишет сообщения (здесь они печатаются на стандартный вывод, но вы также можете легко подключиться к потребителю, который пишет в файл).