我有一个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(略好)。
问:
编辑:
这是一小段运行的内存配置文件(约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/