在 Haskell 的 ST Monad 中高效记录字符串数据



我有一个Haskell程序,它在ST monad内部运行期间生成约280M的日志文本数据。这几乎是所有内存消耗的地方(在禁用日志记录的情况下,程序总共分配3MB的实际内存)。

问题是,我的内存用完了。当程序运行时,内存消耗超过1.5GB,当它试图将日志字符串写入文件时,内存最终会耗尽。

log函数获取一个字符串,并将日志数据累积到存储在环境中的STRef中的字符串生成器中:

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

我尝试使用bang模式和modifySTRef引入严格性,但这会使内存消耗更加严重。

我按照hPutBuilder文档的建议编写日志字符串,如下所示:

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

这会额外消耗几GB的内存。我尝试了不同的缓冲设置,并首先转换为懒惰的ByteString(稍微好一点)。

Qs:

  • 如何在程序运行时最大限度地减少内存消耗?我希望给定一个严格的ByteString表示和适当的严格性,我需要的内存比我存储的大约280M的实际日志数据多一点。

  • 如何在不分配内存的情况下将结果写入文件?我不明白为什么Haskell需要GB的内存来将一些常驻数据流式传输到一个文件中。

编辑:

以下是小规模运行(约42MB的日志数据)的内存配置文件。在禁用日志记录的情况下,总内存使用量为3MB。

    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

我试着添加bang模式,$!,deepseq/$!!,武力等等,但似乎没有任何区别。我如何强迫Haskell实际使用我的string/printf表达式等,并将其放入一个紧凑的ByteString中,而不是保留所有[Char]列表和未求值的thunk?

编辑:

这是实际的全跟踪功能

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 monad中调用这个typeclass函数,我需要注意什么吗?只是为了让它真正被调用,而不是以任何方式被延迟。

do
    trace $ printf "%i" myint

还好吗?

谢谢!

由于日志消息占用了那么多内存,因此在生成日志消息后立即将其写入文件会更高效。这似乎是不可能的,因为我们在ST monad中,在ST Monod中不能执行IO。

但有一个解决办法:使用某种协程monad转换器,就像"管道"包中的那些。以下是一个使用管道的示例-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

它将日志打印到stdout。运行时,它输出以下内容:

Before writing
After writing
False

它的工作原理如下:在生产者中使用respond发出日志消息,同时仍然驻留在ST monad中。这样,你就可以记录并确保你的计算不会执行一些奇怪的IO操作。不过,它会迫使您在代码中添加lifts。

一旦构建了ST计算,就可以使用hoist将生产者的基本monad从ST转换为IO。升降器是一个有用的功能,可以让你在盘子还在桌子上的时候更换桌布。

现在我们在IO的土地上!剩下要做的唯一一件事是将生产者与实际写入消息的消费者连接起来(在这里,它们被打印到stdout,但您也可以很容易地连接到写入文件的消费者。)

最新更新