6

STモナド内での実行中に最大280Mのログテキストデータを生成するHaskellプログラムがあります。これは、事実上すべてのメモリ消費が発生する場所です (ロギングを無効にすると、プログラムは合計 3MB の実メモリを割り当てます)。

問題は、メモリが不足していることです。プログラムの実行中にメモリ消費量が 1.5GB を超え、ログ文字列をファイルに書き込もうとすると、最終的にメモリが不足します。

ログ関数は文字列を受け取り、環境内の STRef に格納されている文字列ビルダーにログ データを蓄積します。

import qualified Data.ByteString.Lazy.Builder as BB
...
myLogFunction s = do
    ...
    lift $ modifySTRef myStringBuilderRef (<> BB.stringUtf8 s)

強打パターンやmodifySTRef'を使って厳密性を導入してみましたが、メモリ消費がさらに悪化しました。

次のように、hPutBuilder のドキュメントで推奨されているように、ログ文字列を書き込みます。

    hSetBinaryMode h True
    hSetBuffering  h $ BlockBuffering Nothing
    BB.hPutBuilder h trace

これにより、さらに数 GB のメモリが消費されます。さまざまなバッファリング設定を試し、最初に遅延 ByteString に変換しました (少し改善されました)。

質問:

  • プログラムの実行中にメモリ消費を最小限に抑えるにはどうすればよいですか? 厳密な ByteString 表現と適切な量の厳密さを考えると、保存している実際のログ データの ~280M よりも少し多くのメモリが必要になると予想されます。

  • メモリを割り当てずに結果をファイルに書き込むにはどうすればよいですか? 常駐データをファイルにストリーミングするだけで、Haskell が何 GB ものメモリを必要とする理由がわかりません。

編集:

小規模な実行 (最大 42 MB のログ データ) のメモリ プロファイルを次に示します。ロギングを無効にした場合、合計メモリ使用量は 3MB です。

    15,632,058,700 bytes allocated in the heap
     4,168,127,708 bytes copied during GC
       343,530,916 bytes maximum residency (42 sample(s))
         7,149,352 bytes maximum slop
               931 MB total memory in use (0 MB lost due to fragmentation)

                                      Tot time (elapsed)  Avg pause  Max pause
    Gen  0     29975 colls,     0 par    5.96s    6.15s     0.0002s    0.0104s
    Gen  1        42 colls,     0 par    6.01s    7.16s     0.1705s    1.5604s

    TASKS: 3 (1 bound, 2 peak workers (2 total), using -N1)

    SPARKS: 0 (0 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)

    INIT    time    0.00s  (  0.00s elapsed)
    MUT     time   32.38s  ( 33.87s elapsed)
    GC      time   11.97s  ( 13.31s elapsed)
    RP      time    0.00s  (  0.00s elapsed)
    PROF    time    0.00s  (  0.00s elapsed)
    EXIT    time    0.00s  (  0.00s elapsed)
    Total   time   44.35s  ( 47.18s elapsed)

    Alloc rate    482,749,347 bytes per MUT second

    Productivity  73.0% of total user, 68.6% of total elapsed

編集:

求められたように、小さなログを実行してメモリ プロファイルを実行しました。

プロフィール http://imageshack.us/a/img14/9778/6a5o.png

該当箇所にバンパターン、$!、deepseq/$!!、force などを入れてみましたが、あまり変わらないようです。これらすべての [Char] リストと未評価のサンクを保持する代わりに、Haskell に実際に文字列/printf 式などを取得させ、それをタイトな ByteString に入れるにはどうすればよいですか?

編集:

これが実際の完全なトレース機能です

trace s = do
     enable <- asks envTraceEnable
     when (enable) $ do
        envtrace <- asks envTrace
        let b = B8.pack s
        lift $ b `seq` modifySTRef' envtrace (<> BB.byteString b)

これは「厳密」で十分ですか?ReaderT/ST モナド内でこの型クラス関数を呼び出す場合、何か注意する必要がありますか? 実際に呼び出され、決して延期されないようにするためです。

do
    trace $ printf "%i" myint

結構です?

ありがとう!

4

1 に答える 1

3

ログ メッセージは大量のメモリを必要とするため、ログ メッセージが生成されたらすぐにファイルに書き込む方が効率的です。ST モナドの内部にいるため、これは不可能に思えます。また、ST モナドにいる間は IO を実行できません。

しかし、解決策があります。「pipes」パッケージのようなある種のコルーチン モナド トランスフォーマーを使用します。これはpipes-3.3.0を使用した例です:

{-# LANGUAGE ExplicitForAll #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE LiberalTypeSynonyms #-}

import Control.Monad
import Control.Monad.ST
import Control.Monad.ST (stToIO) -- Transforms ST computations into IO computations
import Control.Monad.Trans
import Control.Monad.Morph (hoist) -- Changes the base monad of a monad transformer
import Control.Proxy.Prelude (stdoutD) -- Consumer that prints to stdout
import Control.Proxy.Core
import Control.Proxy.Core.Correct

import Data.STRef

simpleST :: ST s Bool
simpleST= do
    ref <- newSTRef True
    writeSTRef ref False
    readSTRef ref

-- Like simpleST, but emits log messages during the computation
loggingST :: Producer ProxyCorrect String (ST s) Bool
loggingST = do
    ref <- lift $ newSTRef True
    respond "Before writing"
    lift $ writeSTRef ref False
    respond "After writing"
    lift $ readSTRef ref

adapt :: (forall s . Producer ProxyCorrect String (ST s) a) ->
         Producer ProxyCorrect String IO a
adapt x = hoist stToIO x

main :: IO ()
main = do
    result <- runProxy $ (\_ -> adapt loggingST) >-> stdoutD
    putStrLn . show $ result

ログを stdout に出力します。実行すると、次のように出力されます。

Before writing
After writing
False

respondこれは次のように機能します: ST モナドに常駐しながら、プロデューサでログ メッセージを発行します。そうすれば、ログを記録し、計算が奇妙な IO 処理を実行しないことを確認できます。ただし、コードにリフトを追加する必要があります。

ST 計算を構築したら、 を使用してプロデューサの基本モナドを ST から IO に変換しますhoist。テーブルに食器を置いたまま、テーブルクロスを交換できる便利なホイスト機能。

今、私たちはIOランドにいます!残された唯一のことは、実際にメッセージを書き込むコンシューマーにプロデューサーを接続することです (ここではメッセージは標準出力に出力されますが、ファイルに書き込むコンシューマーに簡単に接続することもできます)。

于 2013-08-15T12:16:26.447 に答える