NBF*_*RTW 6 logging haskell out-of-memory lazy-evaluation bytestring
我有一个Haskell程序,它在ST monad中运行期间生成~280M的日志文本数据.这几乎是所有内存消耗的地方(禁用日志记录,程序分配总计3MB的实内存).
问题是,我的内存不足.虽然程序运行内存消耗超过1.5GB,但它最终在尝试将日志字符串写入文件时耗尽.
log函数接受String并将日志数据累积到存储在环境中的STRef中的字符串构建器中:
import qualified Data.ByteString.Lazy.Builder as BB
...
myLogFunction s = do
...
lift $ modifySTRef myStringBuilderRef (<> BB.stringUtf8 s)
Run Code Online (Sandbox Code Playgroud)
我尝试使用bang模式和modifySTRef'来引入严格性,但这使得内存消耗更加糟糕.
我按照hPutBuilder文档的建议编写日志字符串,如下所示:
hSetBinaryMode h True
hSetBuffering h $ BlockBuffering Nothing
BB.hPutBuilder h trace
Run Code Online (Sandbox Code Playgroud)
这会消耗几个额外的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
Run Code Online (Sandbox Code Playgroud)
编辑:
我按照要求运行了一个带有小日志运行的内存配置文件:
个人资料http://imageshack.us/a/img14/9778/6a5o.png
我尝试在相关的地方添加爆炸模式,$ !, 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)
Run Code Online (Sandbox Code Playgroud)
这个'严格'够吗?如果我在ReaderT/ST monad中调用这个类型类函数,我是否需要注意什么?只是为了它实际上被调用而不是以任何方式推迟.
do
trace $ printf "%i" myint
Run Code Online (Sandbox Code Playgroud)
很好吗?
谢谢!
由于日志消息占用那么多内存,因此在生成日志消息后立即将其写入文件会更有效。这似乎是不可能的,因为我们位于 ST monad 内部,并且在 ST monad 中无法执行 IO。
但有一个出路:使用某种协程 monad 转换器,例如“pipes”包中的转换器。这是使用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
Run Code Online (Sandbox Code Playgroud)
它将日志打印到标准输出。运行时,它输出以下内容:
Before writing
After writing
False
Run Code Online (Sandbox Code Playgroud)
它的工作原理如下:您在生产者中发出日志消息respond
,同时仍然驻留在 ST monad 中。这样您就可以记录并仍然确保您的计算不会执行一些奇怪的 IO 操作。不过,它迫使您在代码中添加提升。
一旦构建了 ST 计算,您就可以使用 将生产者的基本 monad 从 ST 转换为 IO hoist
。升降机是一项有用的功能,可让您在盘子还在桌子上时更换桌布。
现在我们来到了 IO 王国!剩下要做的唯一一件事就是将生产者与实际写入消息的消费者连接起来(这里它们被打印到标准输出,但您可以轻松地连接到写入文件的消费者。)