我有一个利用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)
无论我使用什么动作,它都会泄漏内存,即使我只是让它打印一个句号。
据我所见,该函数是尾递归的,并且两个计数器都经常被强制执行(我尝试了在其中加入“seq c”和“seq t”,但没有效果)。有线索吗?
如果我放一个“awaitForever”来打印每条记录的报告,那么它就可以正常工作。
更新1:只有在使用-O2编译时才会出现这种情况。分析表明,泄漏的内存是在递归的“skipN”函数中分配的,并且被“SYSTEM”保留(不管那是什么意思)。
更新2:我已经成功解决了它,至少在我的当前程序环境中是这样。我用下面的函数替换了上面的函数。请注意,“proc”是类型为“Int -> Int -> Maybe i -> m ()”:要使用它,您需要调用“await”并传递结果。由于某种原因,交换“await”和“yield”解决了问题。因此,现在它在产生前一结果之前等待下一个输入。
-- | 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中出现了内存泄漏问题,请尝试交换yield和await操作。
skipN
,而是到(>>(yield v)(skipN x y))
。使用单子编写递归例程时,这是一个常见的陷阱。我不确定 GHC 是否能够正确地优化它,除非查看核心转储,但我的初步猜测是您实际上并没有使用尾递归函数。 - bheklilrsum (x:xs) = x + sum xs
不是尾递归的,因为最后一个被调用的函数不是sum
,而是(+)
,因为该等式等同于sum (x:xs) = (+) x xs
。因此,我们经常使用带有累加器参数的辅助函数编写递归函数,或者如果情况足够简单,就直接使用fold
,如sum = go 0 where { go a [] = a; go a (x:xs) a = go (x + a) xs }
或sum = foldl' (+) 0
。由于do
表达式展开成使用>>
和>>=
,这意味着堆栈中的最后一个调用是其中之一,而不是它的第二个参数。 - bheklilrpipes
和conduit
都不需要尾递归才能在恒定的空间内运行。讨论尾递归只是一个转移注意力的话题。 - Gabriella Gonzalez