• 主页
  • 减少Haskell程序中的垃圾收集暂停时间

减少Haskell程序中的垃圾收集暂停时间

我们正在开发一个程序,该程序可以接收和转发“消息”,同时保留这些消息的临时历史记录,以便在需要时可以告诉您消息的历史记录。消息是通过数字标识的,通常大小约为1 KB,我们需要保留成百上千的此类消息。

我们希望针对延迟优化此程序:发送和接收消息之间的时间必须小于10毫秒。

该程序用Haskell编写,并由GHC编译。但是,我们发现,垃圾回收暂停对于我们的延迟要求来说太长了:在我们的实际程序中超过100毫秒。

以下程序是我们应用程序的简化版本。它使用a Data.Map.Strict来存储消息。消息ByteString由标识Int。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毫秒)的“最大暂停”。我们希望将其减少至少一个数量级。

实验表明,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周期“划分”内存?

转载请注明出处:http://www.jubohx.com/article/20230516/911334.html