Nous développons un programme qui reçoit et transmet des "messages", tout en gardant un historique temporaire de ces messages, afin qu'il puisse vous dire l'historique des messages si demandé. Les messages sont identifiés numériquement, mesurent généralement environ 1 kilo-octet et nous devons conserver des centaines de milliers de ces messages.
Nous souhaitons optimiser ce programme pour la latence: le temps entre l'envoi et la réception d'un message doit être inférieur à 10 millisecondes.
Le programme est écrit en Haskell et compilé avec GHC. Cependant, nous avons constaté que les pauses de garbage collection sont beaucoup trop longues pour nos besoins de latence: plus de 100 millisecondes dans notre programme réel.
Le programme suivant est une version simplifiée de notre application. Il utilise un Data.Map.Strict
pour stocker les messages. Les messages sont ByteString
identifiés par un Int
. 1 000 000 de messages sont insérés dans un ordre numérique croissant et les messages les plus anciens sont continuellement supprimés pour conserver l'historique à un maximum de 200 000 messages.
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])
Nous avons compilé et exécuté ce programme en utilisant:
$ 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
La métrique importante ici est la "pause maximale" de 0,0515 s, soit 51 millisecondes. Nous souhaitons réduire cela d'au moins un ordre de grandeur.
L'expérimentation montre que la durée d'une pause GC est déterminée par le nombre de messages dans l'historique. La relation est à peu près linéaire, ou peut-être super-linéaire. Le tableau suivant montre cette relation. ( Vous pouvez voir nos tests d'analyse comparative ici , et quelques graphiques ici .)
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
Nous avons expérimenté plusieurs autres variables pour déterminer si elles peuvent réduire cette latence, dont aucune ne fait une grande différence. Parmi ces variables sans importance figurent: l'optimisation ( -O
, -O2
); Options RTS GC ( -G
, -H
, -A
, -c
), nombre de cœurs ( -N
), différentes structures de données ( Data.Sequence
), la taille des messages, et la quantité de déchets de courte durée produite. Le principal facteur déterminant est le nombre de messages dans l'historique.
Notre théorie de travail est que les pauses sont linéaires dans le nombre de messages car chaque cycle GC doit parcourir toute la mémoire de travail accessible et la copier, qui sont des opérations clairement linéaires.
Des questions:
- Cette théorie du temps linéaire est-elle correcte? La durée des pauses GC peut-elle être exprimée de cette manière simple, ou la réalité est-elle plus complexe?
- Si la pause GC est linéaire dans la mémoire de travail, existe-t-il un moyen de réduire les facteurs constants impliqués?
- Existe-t-il des options pour la GC incrémentielle, ou quelque chose du genre? Nous ne pouvons voir que des documents de recherche. Nous sommes très disposés à échanger le débit contre une latence plus faible.
- Existe-t-il des moyens de «partitionner» la mémoire pour des cycles GC plus petits, autres que la division en plusieurs processus?
COntrol.Concurrent.Chan
par exemple? Les objets mutables changent l'équation)? Je vous suggère de commencer par vous assurer que vous savez quels déchets vous générez et d'en faire le moins possible (par exemple, assurez-vous que la fusion se produit, essayez -funbox-strict
). Essayez peut-être d'utiliser une bibliothèque de streaming (iostreams, tubes, conduit, streaming) et d'appeler performGC
directement à des intervalles plus fréquents.
MutableByteArray
; GC ne sera pas du tout impliqué dans ce cas)