Haskell 为什么包装Data.Binary.Put monad会造成内存泄漏?(第二部分)
在我的示例中,我尝试将Data.Binary.Put monad包装到另一个monad中,以便稍后我可以问它诸如“它将写入多少字节”或“文件中的当前位置是什么”之类的问题 以前,我认为理解为什么它在使用一个微不足道的(IdentityT?)包装器时会泄漏内存会让我解决我的问题。但即使你们帮我解决了这个琐碎的包装器的问题,用一些有用的东西(比如StateT或WriterT)包装它仍然会消耗太多内存(通常会崩溃) 例如,这是我试图包装它的一种方式,它会为大输入泄漏内存: type Out = StateT Integer P.PutM () writeToFile :: String -> Out -> IO () writeToFile path out = BL.writeFile path $ P.runPut $ do runStateT out 0 return () 键入Out=StateT整数P.PutM() writeToFile::字符串->输出->IO() writeToFile path out=BL.writeFile path$P.runPut$do runStateT out 0 返回() 是一个更完整的代码示例,演示了该问题 我想知道的是:Haskell 为什么包装Data.Binary.Put monad会造成内存泄漏?(第二部分),haskell,memory-leaks,binary,monads,monad-transformers,Haskell,Memory Leaks,Binary,Monads,Monad Transformers,在我的示例中,我尝试将Data.Binary.Put monad包装到另一个monad中,以便稍后我可以问它诸如“它将写入多少字节”或“文件中的当前位置是什么”之类的问题 以前,我认为理解为什么它在使用一个微不足道的(IdentityT?)包装器时会泄漏内存会让我解决我的问题。但即使你们帮我解决了这个琐碎的包装器的问题,用一些有用的东西(比如StateT或WriterT)包装它仍然会消耗太多内存(通常会崩溃) 例如,这是我试图包装它的一种方式,它会为大输入泄漏内存: type Out = Sta
有人能进一步帮忙吗?看来monad transformer太懒了。您可以通过以下方式运行程序来创建堆配置文件(无需专门构建):
$ ./myprog +RTS -hT
$ hp2ps myprog.hp
$ open hp2ps.ps # Or whichever viewer you have
在这种情况下,它不是特别有用,因为它只显示大量的PAP
s、FUN\u 1\u 0
s和FUN\u 2\u 0
s。这意味着堆由许多部分应用的函数以及一个参数和两个参数的函数组成。这通常意味着某些东西没有得到足够的评估。Monad变形金刚在这方面有些臭名昭著
解决方法是使用更严格的monad转换器。(his需要{-#语言类型}
newtype MyStateT s m a =
MyStateT { unMyStateT :: forall r. (s -> a -> m r) -> s -> m r }
Continuation passing样式意味着我们不直接返回结果,而是调用另一个函数Continuation,并使用我们的结果,在本例中是s
和a
。实例定义看起来有点滑稽。要理解它,请阅读上面的链接(Wikipedia)
使用如此严格的转换器的缺点是,您无法再定义
MonadFix
实例,某些惰性技巧也不再有效。我开始使用它,并意识到更大的问题是——您的算法具有可怕的复杂性。您不需要一次计算每个子树的大小,而是使用它来计算每次调用getSize时都调用ce。并且递归调用getSize。对于每个叶节点,每次对其父节点调用getSize时都会调用一次getSize。并且每次对其父节点调用getSize时,都会对其本身调用一次getSize+每次对其任何父节点调用getSize时都会调用一次getSize。因此,getSize至少会在树的深度上进行几何调用。您可以需要缓存大小以获得类似于合理运行时的内容
这就是说,这是一个核心函数的版本,它似乎运行正常,没有泄漏,尽管出于上述原因,它确实在爬行:
type MyPut = S (Offset,Size) P.PutM
peal_1 :: (Monad m, Num t, Num t1) => S (t, t1) m a -> m a
peal_1 put = unS put (\o -> return) (0,0)
writeToFile :: String -> MyPut () -> IO ()
writeToFile path put =
BL.writeFile path $ P.runPut $ (peal_1 put) >> return ()
getSize :: MyPut a -> MyPut Int
getSize x = S $ \f os -> unS (x >> getCurrentSize) f os
getCurrentOffset :: MyPut Int
getCurrentOffset = S $ \f os -> f os (fst os)
getCurrentSize :: MyPut Int
getCurrentSize = S $ \f os -> f os (snd os)
我还必须说,我不确定您的逻辑总体上是否正确。我的代码在修复漏洞时保留了当前的行为。我通过在精简的数据集上运行它和您的代码并生成完全相同的文件来测试这一点
但是对于您的大型测试数据,这段代码在我杀死它之前编写了6.5G(在那之前提供的代码已经耗尽了堆)。我怀疑但尚未测试put monad中的底层调用在每次调用getSize时都会运行一次,即使getSize的结果会被丢弃
我建议的适当解决方案是对您的另一个问题的回答:我认为您的代码使用的是StateT的惰性版本,请尝试使用Control.Monad.State.Strict.I(而不是OP)尝试使用
.Strict
但没有改变空间要求。C.M.S.Strict
仅比C.M.S
稍微严格一点。严格性
instance Monad m => Monad (MyStateT s m) where
return x = MyStateT (\k s -> k s x)
MyStateT f >>= kk = MyStateT (\k s ->
f (\s' a -> unMyStateT (kk a) k s') s)
runMyStateT :: Monad m => MyStateT s m a -> s -> m (a, s)
runMyStateT (MyStateT f) s0 = f (\s a -> return (a, s)) s0
instance MonadTrans (MyStateT s) where
lift act = MyStateT (\k s -> do a <- act; k s a)
type Out = MyStateT Integer P.PutM ()
$ ./so1 +RTS -s
begin
end
8,001,343,308 bytes allocated in the heap
877,696,096 bytes copied during GC
46,628 bytes maximum residency (861 sample(s))
33,196 bytes maximum slop
2 MB total memory in use (0 MB lost due to fragmentation)
Generation 0: 14345 collections, 0 parallel, 3.32s, 3.38s elapsed
Generation 1: 861 collections, 0 parallel, 0.08s, 0.08s elapsed
type MyPut = S (Offset,Size) P.PutM
peal_1 :: (Monad m, Num t, Num t1) => S (t, t1) m a -> m a
peal_1 put = unS put (\o -> return) (0,0)
writeToFile :: String -> MyPut () -> IO ()
writeToFile path put =
BL.writeFile path $ P.runPut $ (peal_1 put) >> return ()
getSize :: MyPut a -> MyPut Int
getSize x = S $ \f os -> unS (x >> getCurrentSize) f os
getCurrentOffset :: MyPut Int
getCurrentOffset = S $ \f os -> f os (fst os)
getCurrentSize :: MyPut Int
getCurrentSize = S $ \f os -> f os (snd os)