我有一个Haskell程序,在ST monad内运行时会生成约280M的记录文本数据。这实际上是所有内存消耗的地方(禁用日志记录后,程序将分配总计3MB的实际内存)。

问题是,我的内存不足。当程序运行时,内存消耗超过1.5GB,并且在尝试将日志字符串写入文件时最终耗尽了内存。

日志功能采用String并将日志数据累积到环境中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

这会消耗几个额外的GB内存。我尝试了不同的缓冲设置,然后首先转换为惰性ByteString(略好)。

问:
  • 如何在程序运行时最大程度地减少内存消耗?我希望给定一个紧密的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
    

    编辑:

    我运行了一个内存配置文件,并按要求运行了一个小日志:

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

    我尝试在相关位置添加爆炸模式,$ !、 deepseq/$ !!,force等,但似乎没有任何区别。我该如何强制Haskell实际使用我的字符串/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 monad中调用此typeclass函数,是否需要注意任何事项?只是为了使它实际上被调用而不以任何方式延迟。
    do
        trace $ printf "%i" myint
    

    可以吗

    谢谢!

    最佳答案

    由于日志消息占用了如此多的内存,因此在产生日志消息后立即将其写入文件会更加有效。这似乎是不可能的,因为我们在ST monad内部,并且您无法在ST monad中执行IO。

    但是有一个出路:使用某种“协程” monad变压器,例如“管道”包装中的那些。这是使用pipes-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 monad中。这样一来,您就可以记录日志,并仍然确保您的计算不会执行某些奇怪的IO东西。但是,它会迫使您使用升降机添加代码。

    一旦构建了ST计算,就可以使用hoist将生产者的基本monad从ST转换为IO。 hoist是一个有用的功能,可让您在碗碟还在 table 上时更换 table 布。

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

    关于haskell - 在Haskell的ST Monad中高效记录字符串数据,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/18249338/

    10-15 23:49