我们正在开发一个程序,该程序可以接收和转发“消息”,同时保留这些消息的临时历史记录,以便在需要时可以告诉您消息的历史记录。消息是通过数字标识的,通常大小约为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周期“划分”内存?
COntrol.Concurrent.Chan
吗(例如您没有使用?可变对象会改变方程式)?我建议首先确保您知道正在生成的垃圾,并尽可能减少垃圾(例如,确保发生融合,请尝试-funbox-strict
)。也许尝试使用流媒体库(iostream,管道,管道,流媒体),并performGC
以更频繁的时间间隔直接调用。
MutableByteArray
;的环形缓冲区;在这种情况下,GC根本不涉及)