我们希望将此程序优化为延迟时间:发送和接收消息之间的时间必须低于10毫秒。
该程序是用Haskell编写并使用GHC编译的。但是,我们发现在实际程序中,垃圾回收暂停时间太长,超过了我们的延迟要求:超过100毫秒。
以下程序是我们应用程序的简化版本。它使用Data.Map.Strict来存储消息。消息是由Int标识的ByteString。插入100万条递增数字顺序的消息,并不断删除最旧的消息,以使历史记录最多达到20万条。
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
)。也许尝试使用流处理库(iostreams、pipes、conduit、streaming),并在更频繁的间隔直接调用performGC
。 - jberrymanMutableByteArray
中的环形缓冲区;在这种情况下完全不涉及GC)。 - jberryman