为什么这会导致Haskell Conduit库中的内存泄漏?

时间:2014-07-16 16:30:37

标签: haskell memory-leaks conduit

我有一个conduit管道处理一个长文件。我想每1000条记录为用户打印一份进度报告,所以我写了这个:

-- | Every n records, perform the IO action.
-- Used for progress reports to the user.
progress :: (MonadIO m) => Int -> (Int -> i -> IO ()) -> Conduit i m i
progress n act = skipN n 1
   where
      skipN c t = do
         mv <- await
         case mv of
            Nothing -> return ()
            Just v ->
               if c <= 1
                  then do
                     liftIO $ act t v
                     yield v
                     skipN n (succ t)
                  else do
                     yield v
                     skipN (pred c) (succ t)

无论我怎么称呼它,它都会泄漏内存,即使我只是告诉它打印一个句号。

据我所知,该函数是尾递归的,并且两个计数器都是经常强制的(我试过把#34; seq c&#34;&#34; seq t&#34; in,无济于事)。任何线索?

如果我放入&#34; awaitForever&#34;为每条记录打印一份报告,然后就可以了。

Update 1 :只有在使用-O2编译时才会发生这种情况。分析表明泄漏的内存分配在递归&#34; skipN&#34;功能并被&#34; SYSTEM&#34;保留(无论那意味着什么)。

更新2 :我已经成功治愈了它,至少在我目前的计划中是这样。我用这个替换了上面的功能。请注意&#34; proc&#34;属于&#34; Int - &gt; Int - &gt;也许我 - &gt; m()&#34;:使用它你打电话&#34;等待&#34;并传递结果。出于某种原因,交换了#34; await&#34;和&#34;产量&#34;解决了这个问题。所以现在它在产生前一个结果之前等待下一个输入。

-- | Every n records, perform the monadic action. 
-- Used for progress reports to the user.
progress :: (MonadIO m) => Int -> (Int -> i -> IO ()) -> Conduit i m i
progress n act = await >>= proc 1 n
   where
      proc c t = seq c $ seq t $ maybe (return ()) $ \v ->
         if c <= 1
            then {-# SCC "progress.then" #-} do
               liftIO $ act t v
               v1 <- await
               yield v
               proc n (succ t) v1
            else {-# SCC "progress.else" #-} do
               v1 <- await
               yield v
               proc (pred c) (succ t) v1

因此,如果您在Conduit中发生内存泄漏,请尝试交换产量并等待操作。

3 个答案:

答案 0 :(得分:7)

这不是一个anwser,但它是我为测试而破解的一些完整代码。我根本不知道管道,所以它可能不是最好的管道代码。我强迫所有看起来需要被迫的东西,但它仍然会泄漏。

{-# LANGUAGE BangPatterns #-}

import Data.Conduit
import Data.Conduit.List
import Control.Monad.IO.Class

-- | Every n records, perform the IO action.
--   Used for progress reports to the user.
progress :: (MonadIO m) => Int -> (Int -> i -> IO ()) -> Conduit i m i
progress n act = skipN n 1
   where
      skipN !c !t = do
         mv <- await
         case mv of
            Nothing -> return ()
            Just !v ->
               if (c :: Int) <= 1
                  then do
                     liftIO $ act t v
                     yield v
                     skipN n (succ t)
                  else do
                     yield v
                     skipN (pred c) (succ t)

main :: IO ()
main = unfold (\b -> b `seq` Just (b, b+1)) 1
       $= progress 100000 (\_ b -> print b)
       $$ fold (\_ _ -> ()) ()

另一方面,

main = unfold (\b -> b `seq` Just (b, b+1)) 1 $$ fold (\_ _ -> ()) ()

没有泄漏,所以progress中的某些内容确实似乎是问题所在。我看不清楚。

编辑:泄漏只发生在ghci!如果我编译一个二进制文件并运行它就没有泄漏(我应该先测试一下......)

答案 1 :(得分:5)

我认为汤姆的答案是正确的,我将此作为一个单独的答案开始,因为它可能会引入一些新的讨论(因为它只是一个评论太长了)。在我的测试中,用print b替换Tom示例中的return ()可以消除内存泄漏。这让我觉得问题实际上是print,而不是conduit。为了测试这个理论,我在C中编写了一个简单的辅助函数(放在helper.c中):

#include <stdio.h>

void helper(int c)
{
    printf("%d\n", c);
}

然后我在Haskell代码中输入了这个函数:

foreign import ccall "helper" helper :: Int -> IO ()

我通过调用print将呼叫替换为helper。程序的输出是相同的,但我没有泄漏,最大驻留时间为32kb vs 62kb(我还修改了代码以停止在10m记录以便更好地进行比较)。

当我完全切断导管时,我看到了类似的行为,例如:

main :: IO ()
main = forM_ [1..10000000] $ \i ->
    when (i `mod` 100000 == 0) (helper i)

然而,我并不相信这是printHandle中的错误。我的测试从未显示泄漏达到任何实质内存使用量,因此它可能只是缓冲区正在向限制增长。我需要做更多的研究才能更好地理解这一点,但我想首先看看这个分析是否与其他人看到的一致。

答案 2 :(得分:1)

我知道这是两年之后,但我怀疑发生了什么事情是完全懒惰正在提升身体的一部分等待到等待之前,这导致空间泄漏。它看起来类似于m y blog post on this very topic中“增加共享”部分中的情况。