【问题标题】:Efficient logging of string data in Haskell's ST Monad在 Haskell 的 ST Monad 中高效记录字符串数据
【发布时间】:2013-08-17 10:40:32
【问题描述】:

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

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

log 函数接受一个 String 并将日志数据累积到一个 string builder 中,存储在环境中的 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 表示和适当的严格性,我需要的内存比我存储的大约 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

我尝试在相关位置添加 bang 模式、$!、deepseq/$!!、force 等,但似乎没有任何区别。我如何强制 Haskell 实际获取我的字符串 / 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 中调用这个类型类函数,我需要注意什么吗?只是为了让它被实际调用而不是以任何方式延迟。

do
    trace $ printf "%i" myint

还好吗?

谢谢!

【问题讨论】:

  • 日志记录与状态无关,因此我建议您使用 Writer monad
  • 即使从使用带有 STRef 的 reader monad 转换为 writer 我也有同样的情况。最后它是一个 Builder 类型的幺半群。我宁愿不要无缘无故地在转换器堆栈中添加 WriterT。
  • 我们需要更多数据。你能给我们看一个堆配置文件吗?您的日志是如何生成的?例如,如果您使用stringUtf8,那么我怀疑生成的Builder 包含大量对String 的引用,这就是内存的去向。
  • 顺便说一句,问题几乎可以肯定是过度懒惰并且不了解Builder 表示的细节。强制 Builder 什么都不做。如果ByteString 在其构造中具有对象图,则强制将ByteString 添加到其中至关重要。
  • @AN1 正如我所怀疑的那样,您的堆配置文件显示普通 Strings 消耗了大量内存。我同意@Carl 的观点,这看起来像是过分的懒惰。顺便说一句,您可以使用hp2ps -c 生成颜色输出。

标签: haskell logging out-of-memory lazy-evaluation bytestring


【解决方案1】:

由于日志消息占用这么多内存,因此在生成后立即将它们写入文件会更有效。这似乎是不可能的,因为我们在 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 是一个有用的功能,可让您在餐具还在桌子上时更换桌布。

现在我们在 IO 领域!剩下要做的就是将生产者与实际写入消息的消费者连接起来(这里它们被打印到标准输出,但您可以很容易地连接到写入文件的消费者。)

【讨论】:

  • 我不得不承认这有点过头了,但我一直想看看管道包!但为了清楚起见,日志消息的大小绝对没有问题。 280MB 完全没问题!问题是 Haskell 在“Haskell 的东西”上浪费了 GB。如果我使用具有 500MB 的 Word8s 的可变未装箱向量实现我的日志记录并将其写入文件,一切都会好起来的。我只是想问是否有人可以告诉我为什么 ByteString 库需要比预期多 5 倍的存储空间,然后分配 GB 的临时内存只是为了将该数据写入文件。
猜你喜欢
  • 1970-01-01
  • 2019-01-23
  • 2011-10-13
  • 2013-06-12
  • 2019-02-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-05-30
相关资源
最近更新 更多