2016-12-23 8 views
16

というライブラリを作成しました。workerという関数を提供し、メッセージが見つかったときにハンドラを呼び出して、メッセージキュー(RabbitMQなど)をポーリングします。それからポーリングに戻ります。再帰的IO関数のメモリリーク - PAP

メモリがリークしています。私はそれをプロファイリングし、グラフはPAP(部分関数アプリケーション)が原因だと言います。 私のコードのどこに漏れがありますか? IOでループするときに、漏れを避けるにはどうすればいいですか?forever?ここで

enter image description here

いくつかの関連する機能があります。 The full source is here

Example Program。これは、メモリリークがpollであっ

main :: IO() 
main = do 
    -- connect 
    conn <- Worker.connect (fromURI "amqp://guest:[email protected]: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 
+0

散策あなたのghcのバージョンとはどのようにコンパイルしていますか? – jberryman

+1

これはlts-7.3に設定されているので、GHC 8.0.1です。私はstack install --profileでコンパイルしています。しかし、私は通常のスタックのインストールでメモリリークを取得します。スタックテンプレートのデフォルトのghcオプションを使用する:-threaded -rtsopts -with-rtsopts = -N –

+2

この例は非常に遠くありません。サンプルプログラムでライブラリ全体( 'Network.AMQP.Worker')をインポートしています。それがそうであるように、これは広すぎます。 – user2407038

答えて

14

はあなたの問題を示して非常に単純な例です:

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を書きました。 Reidが彼の 答えで指摘しているように、簡単な要約は、 >>の部分アプリケーションのチェーンを作成して覚えていることです。

私はまたこれについてghc ticketを提出しました。

3

をリーク。 monad-loopsを使用して、次のように定義を変更しました。untilJustは私の再帰と同じことをしますが、リークを修正します。

私の以前の定義pollのメモリがリークしていた理由について誰でもコメントできますか?ここで

{-# LANGUAGE FlexibleContexts #-} 

module Network.AMQP.Worker.Poll where 

import Control.Concurrent (threadDelay) 
import Control.Monad.Trans.Control (MonadBaseControl) 
import Control.Monad.Base (liftBase) 
import Control.Monad.Loops (untilJust) 

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

は多分理解しやすい例では、両方のfgへの参照を(それは基本的に上の機能としてfの組成とgのしているクロージャのいくつかの種類のIOアクションのf >> gの評価この1

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) 

利回りです状態トークン)。 count 0は、return() >> return() >> return() >> ...という形式のクロージャの大きな構造に評価されるサンクcを返します。 cを実行すると、この構造が構築されます。cを2回実行する必要があるため、構造全体がまだ生きています。したがって、このプログラムはメモリをリークします(最適化フラグに関係なく)。

countIOに特化されており、最適化が有効になっている場合、GHCにはこのデータ構造の構築を避けるためのさまざまな技があります。しかし彼らはすべて、モナドがIOであることを知ることに頼っています。元count :: Monad m => Int -> m()に戻って

、我々はそうcは、ほんの少しの構造return() >>= (\_ -> BODY)で、今再帰呼び出しがラムダの内側に隠されている

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

に最後の行を変更することにより、この大きな構造の構築を回避しようとすることができます。これは実際に最適化なしでコンパイルするときにスペースリークを回避します。最適化が有効になっているとき(それは引数に依存しないため)ただし、GHCは

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

、今cを生産ラムダの本体からcount (n+1)を出し浮かぶ再び大規模な構造物である...

+0

'NOINLINE'を使用すると、プログラムは元のリーキーなものに匹敵するのですか? – Michael

+0

GHCはインライン化または特殊化しない一般的なケースです(関数が別のモジュールで定義されている場合など)。GHCは多くのトリックを認識しています。トリックを使用すると、さらに最小化できます。 –