递归IO函数中的内存泄漏 - PAP

时间:2016-12-23 18:57:19

标签: haskell recursion memory-leaks profiling

我编写了一个名为amqp-worker的库,它提供了一个名为worker的函数,它为消息轮询消息队列(如RabbitMQ),在发现消息时调用处理程序。然后它回到投票。

它正在泄漏记忆。我已对它进行了分析,图表显示PAP(部分功能应用程序)是罪魁祸首。 我的代码泄漏在哪里?使用IO forever进行循环时如何避免泄漏?

enter image description here

以下是一些相关功能。 The full source is here

Example Program。泄漏

main :: IO ()
main = do
  -- connect
  conn <- Worker.connect (fromURI "amqp://guest:guest@localhost:5672")

  -- initialize the queues
  Worker.initQueue conn queue
  Worker.initQueue conn results

  -- publish a message
  Worker.publish conn queue (TestMessage "hello world")

  -- create a worker, the program loops here
  Worker.worker def conn queue onError (onMessage conn)

worker

worker :: (FromJSON a, MonadBaseControl IO m, MonadCatch m) => WorkerOptions -> Connection -> Queue key a -> (WorkerException SomeException -> m ()) -> (Message a -> m ()) -> m ()
worker opts conn queue onError action =
  forever $ do
    eres <- consumeNext (pollDelay opts) conn queue
    case eres of
      Error (ParseError reason bd) ->
        onError (MessageParseError bd reason)

      Parsed msg ->
        catch
          (action msg)
          (onError . OtherException (body msg))
    liftBase $ threadDelay (loopDelay opts)

consumeNext

consumeNext :: (FromJSON msg, MonadBaseControl IO m) => Microseconds -> Connection -> Queue key msg -> m (ConsumeResult msg)
consumeNext pd conn queue =
    poll pd $ consume conn queue

poll

poll :: (MonadBaseControl IO m) => Int -> m (Maybe a) -> m a
poll us action = do
    ma <- action
    case ma of
      Just a -> return a
      Nothing -> do
        liftBase $ threadDelay us
        poll us action

3 个答案:

答案 0 :(得分:15)

这是一个非常简单的示例,用于演示您的问题:

main :: IO ()
main = worker

{-# NOINLINE worker #-}
worker :: (Monad m) => m ()
worker =
  let loop = poll >> loop
  in loop

poll :: (Monad m) => m a
poll = return () >> poll
If you remove the `NOINLINE`, or specialize `m` to
`IO` (while compiling with `-O`), the leak goes away.

我写了一篇详细的blog post来解释原因 这个代码正好泄漏了内存。快速总结是,正如里德指出的那样 回答,代码创建并记住了一系列部分应用程序 >>秒。

我还提交了一份关于此问题的ghc ticket

答案 1 :(得分:4)

也许一个更容易理解的例子就是这个

main :: IO ()
main = let c = count 0
       in c >> c

{-# NOINLINE count #-}
count :: Monad m => Int -> m ()
count 1000000 = return ()
count n = return () >> count (n+1)

评估f >> g的IO操作会产生某种类型的闭包,它引用fg(它基本上是fg的组合作为状态令牌的功能)。 count 0会返回一个thunk c,它将评估return () >> return () >> return () >> ...形式的大型闭包结构。当我们执行c时,我们构建了这个结构,因为我们必须第二次执行c整个结构仍然存在。所以这个程序会泄漏内存(无论优化标志如何)。

count专门用于IO并且启用了优化时,GHC可以使用各种技巧来避免构建此数据结构;但他们都依赖于知道monad是IO

回到原来的count :: Monad m => Int -> m (),我们可以尝试通过将最后一行更改为

来避免构建这个大结构
count n = return () >>= (\_ -> count (n+1))

现在递归调用隐藏在lambda中,因此c只是一个小结构return () >>= (\_ -> BODY)。这实际上避免了在没有优化的情况下进行编译时的空间泄漏。但是,当启用优化时,GHC从lambda的主体中浮出count (n+1)(因为它不依赖于参数)生成

count n = return () >>= (let body = count (n+1) in \_ -> body)

现在c又是一个大型结构......

答案 2 :(得分:3)

内存泄漏位于CREATE OR REPLACE VIEW your_view AS SELECT item, step_1, step_2, step_3, step_4, step_5, ( CASE WHEN step_1 IN ('OK', 'N/A') THEN 1 ELSE 0 END + CASE WHEN step_2 IN ('OK', 'N/A') THEN 1 ELSE 0 END + CASE WHEN step_3 IN ('OK', 'N/A') THEN 1 ELSE 0 END + CASE WHEN step_4 IN ('OK', 'N/A') THEN 1 ELSE 0 END + CASE WHEN step_5 IN ('OK', 'N/A') THEN 1 ELSE 0 END) / 5 * 100 || '%' AS progress FROM your_table 。使用monad-loops,我将定义更改为以下内容:step看起来像我的递归一样,但修复了泄漏。

有人可以评论为什么我之前对poll的定义是在泄漏记忆吗?

untilJust