最佳答案
我们正在开发一个接收和转发“消息”的程序,同时保留这些消息的临时历史记录,以便它可以告诉您的消息历史,如果需要的话。消息是通过数字识别的,通常大小在1 KB 左右,我们需要保存成千上万的这样的消息。
我们希望优化这个程序的延迟: 发送和接收消息之间的时间必须小于10毫秒。
该程序是在哈斯克尔编写的,并与 GHc 一起编译。然而,我们发现垃圾收集暂停对于我们的延迟需求来说太长了: 在我们的实际程序中超过100毫秒。
下面的程序是我们的应用程序的简化版本。它使用 Data.Map.Strict
来存储消息。消息是由 Int
标识的 ByteString
。以递增的数字顺序插入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 周期必须遍历所有可访问的工作内存并复制它,这显然是线性操作。
问题: