web-dev-qa-db-ja.com

Haskellプログラムでのガベージコレクションの休止時間の短縮

要求された場合にメッセージ履歴を通知できるように、「メッセージ」を受信して​​転送するプログラムを開発していますが、これらのメッセージの一時的な履歴を保持しています。メッセージは数値で識別され、通常サイズは約1キロバイトであり、これらのメッセージを数十万個保持する必要があります。

このプログラムを最適化して待ち時間を短縮します。メッセージの送信と受信の間の時間は10ミリ秒未満でなければなりません。

このプログラムはHaskellで記述され、GHCでコンパイルされています。ただし、ガベージコレクションの一時停止は待機時間の要件に対して非常に長いことがわかりました。実際のプログラムでは100ミリ秒を超えています。

次のプログラムは、アプリケーションの簡易バージョンです。 Data.Map.Strictを使用してメッセージを保存します。メッセージは、ByteStringで識別されるIntsです。 1,000,000のメッセージが数値の昇順で挿入され、履歴を最大200,000のメッセージに保つために、最も古いメッセージが継続的に削除されます。

module Main (main) where

import qualified Control.Exception as Exception
import qualified Control.Monad as Monad
import qualified Data.ByteString as ByteString
import qualified Data.Map.Strict as Map

data Msg = Msg !Int !ByteString.ByteString

type Chan = Map.Map Int ByteString.ByteString

message :: Int -> Msg
message n = Msg n (ByteString.replicate 1024 (fromIntegral n))

pushMsg :: Chan -> Msg -> IO Chan
pushMsg chan (Msg msgId msgContent) =
  Exception.evaluate $
    let
      inserted = Map.insert msgId msgContent chan
    in
      if 200000 < Map.size inserted
      then Map.deleteMin inserted
      else inserted

main :: IO ()
main = Monad.foldM_ pushMsg Map.empty (map message [1..1000000])

以下を使用してこのプログラムをコンパイルして実行しました。

$ ghc --version
The Glorious Glasgow Haskell Compilation System, version 7.10.3
$ ghc -O2 -optc-O3 Main.hs
$ ./Main +RTS -s
   3,116,460,096 bytes allocated in the heap
     385,101,600 bytes copied during GC
     235,234,800 bytes maximum residency (14 sample(s))
     124,137,808 bytes maximum slop
             600 MB total memory in use (0 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0      6558 colls,     0 par    0.238s   0.280s     0.0000s    0.0012s
  Gen  1        14 colls,     0 par    0.179s   0.250s     0.0179s    0.0515s

  INIT    time    0.000s  (  0.000s elapsed)
  MUT     time    0.652s  (  0.745s elapsed)
  GC      time    0.417s  (  0.530s elapsed)
  EXIT    time    0.010s  (  0.052s elapsed)
  Total   time    1.079s  (  1.326s elapsed)

  %GC     time      38.6%  (40.0% elapsed)

  Alloc rate    4,780,213,353 bytes per MUT second

  Productivity  61.4% of total user, 49.9% of total elapsed

ここで重要なメトリックは、0.0515秒(51ミリ秒)の「最大一時停止」です。これを少なくとも1桁削減したいと考えています。

実験では、GCの一時停止の長さは、履歴内のメッセージの数によって決定されることが示されています。関係はほぼ線形、またはおそらく超線形です。次の表に、この関係を示します。 ( ベンチマークテストはこちらで確認できます 、および ここでいくつかのグラフ 。)

msgs history length  max GC pause (ms)
===================  =================
12500                                3
25000                                6
50000                               13
100000                              30
200000                              56
400000                             104
800000                             199
1600000                            487
3200000                           1957
6400000                           5378

他のいくつかの変数を試して、このレイテンシを減らすことができるかどうかを確認しましたが、どれも大きな違いはありません。これらの重要でない変数には次のものがあります。最適化(-O-O2); RTS GCオプション(-G-H-A-c)、コア数(-N)、さまざまなデータ構造(Data.Sequence) 、メッセージのサイズ、生成される短命のゴミの量。圧倒的な決定要因は、履歴内のメッセージの数です。

私たちの動作理論は、各GCサイクルがすべての動作可能なアクセス可能なメモリを調べてコピーする必要があるため、一時停止はメッセージ数で線形であるというものです。

質問:

  • この線形時間理論は正しいですか? GCの一時停止の長さをこの単純な方法で表現できますか、それとも現実はもっと複雑ですか?
  • GCの一時停止が作業メモリ内で線形である場合、関連する一定の要因を減らす方法はありますか?
  • インクリメンタルGCのオプションはありますか?私たちは研究論文しか見ることができません。スループットを低レイテンシーに引き換えます。
  • 複数のプロセスに分割する以外に、GCサイクルを小さくするためにメモリを「分割」する方法はありますか?
123
jameshfisher

実際には、200Mbを超えるライブデータで51ミリ秒の一時停止時間を設定できます。私が取り組んでいるシステムでは、最大一時停止時間が長く、その半分の量のライブデータがあります。

あなたの仮定は正しいです、主要なGCの休止時間はライブデータの量に正比例します、そして残念なことに現状のGHCではそれを回避する方法はありません。過去にインクリメンタルGCを試しましたが、これは研究プロジェクトであり、リリースされたGHCに組み込むのに必要な成熟度に達しませんでした。

将来的にこれを支援することを期待していることの1つは、コンパクトな領域です。 https://phabricator.haskell.org/D1264 。これは、ヒープ内の構造を圧縮する手動のメモリ管理の一種であり、GCはそれを走査する必要がありません。存続期間の長いデータに最適ですが、設定内の個々のメッセージに使用するのに十分でしょう。 GHC 8.2.0でそれを目指しています。

分散設定で、ある種のロードバランサーを使用している場合、一時停止のヒットを回避するためにプレイできるトリックがあります。メジャーGCを実行し、もちろん、マシンが要求を受け取っていなくても、GCがまだ完了していることを確認してください。

89
Simon Marlow

基になるデータ構造としてIOVectorを使用するリングバッファーアプローチでコードスニペットを試しました。私のシステム(GHC 7.10.3、同じコンパイルオプション)では、これにより、最大時間(OPで言及したメトリック)が約22%削減されました。

NB。ここで2つの仮定を行いました。

  1. 可変データ構造は問題に適しています(メッセージの受け渡しはIOとにかく)
  2. あなたのmessageIdは連続的です

いくつかの追加のIntパラメーターと算術(messageIdが0またはminBoundにリセットされるときなど)を使用すると、特定のメッセージがまだ履歴にあるかどうかを判断し、リングバッファ内の対応するインデックス。

テストの楽しみのために:

import qualified Control.Exception as Exception
import qualified Control.Monad as Monad
import qualified Data.ByteString as ByteString
import qualified Data.Map.Strict as Map

import qualified Data.Vector.Mutable as Vector

data Msg = Msg !Int !ByteString.ByteString

type Chan = Map.Map Int ByteString.ByteString

data Chan2 = Chan2
    { next          :: !Int
    , maxId         :: !Int
    , ringBuffer    :: !(Vector.IOVector ByteString.ByteString)
    }

chanSize :: Int
chanSize = 200000

message :: Int -> Msg
message n = Msg n (ByteString.replicate 1024 (fromIntegral n))


newChan2 :: IO Chan2
newChan2 = Chan2 0 0 <$> Vector.unsafeNew chanSize

pushMsg2 :: Chan2 -> Msg -> IO Chan2
pushMsg2 (Chan2 ix _ store) (Msg msgId msgContent) =
    let ix' = if ix == chanSize then 0 else ix + 1
    in Vector.unsafeWrite store ix' msgContent >> return (Chan2 ix' msgId store)

pushMsg :: Chan -> Msg -> IO Chan
pushMsg chan (Msg msgId msgContent) =
  Exception.evaluate $
    let
      inserted = Map.insert msgId msgContent chan
    in
      if chanSize < Map.size inserted
      then Map.deleteMin inserted
      else inserted

main, main1, main2 :: IO ()

main = main2

main1 = Monad.foldM_ pushMsg Map.empty (map message [1..1000000])

main2 = newChan2 >>= \c -> Monad.foldM_ pushMsg2 c (map message [1..1000000])
9
mgmeier

私は他の人に同意する必要があります-リアルタイムの厳しい制約がある場合、GC言語を使用することは理想的ではありません。

ただし、Data.Mapだけでなく、他の利用可能なデータ構造の実験を検討することもできます。

Data.Sequenceを使用して書き直し、いくつかの有望な改善を得ました。

msgs history length  max GC pause (ms)
===================  =================
12500                              0.7
25000                              1.4
50000                              2.8
100000                             5.4
200000                            10.9
400000                            21.8
800000                            46
1600000                           87
3200000                          175
6400000                          350

レイテンシーを最適化していますが、他のメトリックも改善されていることに気付きました。 200000の場合、実行時間は1.5秒から0.2秒に減少し、合計メモリ使用量は600MBから27MBに減少します。

デザインを微調整してごまかしたことに注意してください。

  • IntからMsgを削除したので、2つの場所にありません。
  • IntsからByteStringsへのマップを使用する代わりに、SequencesのByteStringを使用し、メッセージごとに1つのIntの代わりに、 Int全体に対して1つのSequenceでできると思います。メッセージの順序を変更できないと仮定すると、単一のオフセットを使用して、どのメッセージをキュー内のどこに配置するかを変換できます。

(それを示すために、追加の関数getMsgを含めました。)

{-# LANGUAGE BangPatterns #-}

import qualified Control.Exception as Exception
import qualified Control.Monad as Monad
import qualified Data.ByteString as ByteString
import Data.Sequence as S

newtype Msg = Msg ByteString.ByteString

data Chan = Chan Int (Seq ByteString.ByteString)

message :: Int -> Msg
message n = Msg (ByteString.replicate 1024 (fromIntegral n))

maxSize :: Int
maxSize = 200000

pushMsg :: Chan -> Msg -> IO Chan
pushMsg (Chan !offset sq) (Msg msgContent) =
    Exception.evaluate $
        let newSize = 1 + S.length sq
            newSq = sq |> msgContent
        in
        if newSize <= maxSize
            then Chan offset newSq
            else
                case S.viewl newSq of
                    (_ :< newSq') -> Chan (offset+1) newSq'
                    S.EmptyL -> error "Can't happen"

getMsg :: Chan -> Int -> Maybe Msg
getMsg (Chan offset sq) i_ = getMsg' (i_ - offset)
    where
    getMsg' i
        | i < 0            = Nothing
        | i >= S.length sq = Nothing
        | otherwise        = Just (Msg (S.index sq i))

main :: IO ()
main = Monad.foldM_ pushMsg (Chan 0 S.empty) (map message [1..5 * maxSize])
8
John H

さて、GCの言語の制限を見つけました:それらは、筋金入りのリアルタイムシステムには適していません。

次の2つのオプションがあります。

1ヒープサイズを増やし、2レベルのキャッシュシステムを使用します。最も古いメッセージはディスクに送信され、最新のメッセージをメモリに保持します。OSページングを使用してこれを行うことができます。問題は、このソリューションでは、使用されるセカンダリメモリユニットの読み取り機能によっては、ページングが高価になる可能性があることです。

「C」を使用してソリューションをプログラムし、FFIとhaskellに接続します。そうすれば、独自のメモリ管理を行うことができます。これは、必要なメモリを自分で制御できるため、最適なオプションです。