Wir entwickeln ein Programm, das "Nachrichten" empfängt und weiterleitet und dabei einen temporären Verlauf dieser Nachrichten führt, damit es Ihnen auf Anfrage den Nachrichtenverlauf mitteilen kann. Nachrichten werden numerisch identifiziert, haben normalerweise eine Größe von etwa 1 Kilobyte und wir müssen Hunderttausende dieser Nachrichten aufbewahren.
Wir möchten dieses Programm auf Latenz optimieren: Die Zeit zwischen dem Senden und Empfangen einer Nachricht muss unter 10 Millisekunden liegen.
Das Programm ist in Haskell geschrieben und mit GHC kompiliert. Wir haben jedoch festgestellt, dass die Speicherbereinigungspausen für unsere Latenzanforderungen viel zu lang sind: Über 100 Millisekunden in unserem realen Programm.
Das folgende Programm ist eine vereinfachte Version unserer Anwendung. Es verwendet a Data.Map.Strict
zum Speichern von Nachrichten. Nachrichten werden ByteString
durch ein gekennzeichnet Int
. 1.000.000 Nachrichten werden in aufsteigender numerischer Reihenfolge eingefügt, und die ältesten Nachrichten werden kontinuierlich entfernt, um den Verlauf auf maximal 200.000 Nachrichten zu beschränken.
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])
Wir haben dieses Programm kompiliert und ausgeführt mit:
$ 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
Die wichtige Metrik hier ist die "maximale Pause" von 0,0515 s oder 51 Millisekunden. Wir wollen dies um mindestens eine Größenordnung reduzieren.
Experimente zeigen, dass die Länge einer GC-Pause durch die Anzahl der Nachrichten im Verlauf bestimmt wird. Die Beziehung ist ungefähr linear oder vielleicht superlinear. Die folgende Tabelle zeigt diese Beziehung. ( Sie können unsere Benchmarking-Tests hier und einige Diagramme hier sehen .)
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
Wir haben mit mehreren anderen Variablen experimentiert, um herauszufinden, ob sie diese Latenz reduzieren können, von denen keine einen großen Unterschied macht. Zu diesen unwichtigen Variablen gehören: Optimierung ( -O
, -O2
); RTS GC - Optionen ( -G
, -H
, -A
, -c
), Anzahl der Kerne ( -N
), verschiedene Datenstrukturen ( Data.Sequence
), die Größe der Nachrichten, und die Menge an erzeugtem kurzlebig Müll. Der überwältigende bestimmende Faktor ist die Anzahl der Nachrichten im Verlauf.
Unsere Arbeitstheorie besagt, dass die Pausen in der Anzahl der Nachrichten linear sind, da jeder GC-Zyklus den gesamten arbeitssicheren Speicher durchlaufen und kopieren muss, was eindeutig lineare Operationen sind.
Fragen:
- Ist diese lineare Zeittheorie richtig? Kann die Länge der GC-Pausen auf diese einfache Weise ausgedrückt werden oder ist die Realität komplexer?
- Wenn die GC-Pause im Arbeitsspeicher linear ist, gibt es eine Möglichkeit, die konstanten Faktoren zu reduzieren?
- Gibt es Optionen für inkrementelle GC oder ähnliches? Wir können nur Forschungsarbeiten sehen. Wir sind sehr bereit, den Durchsatz gegen eine geringere Latenz zu tauschen.
- Gibt es andere Möglichkeiten, den Speicher für kleinere GC-Zyklen zu "partitionieren", als ihn in mehrere Prozesse aufzuteilen?
COntrol.Concurrent.Chan
? Veränderbare Objekte ändern die Gleichung)? Ich würde vorschlagen, zunächst sicherzustellen, dass Sie wissen, welchen Müll Sie erzeugen, und so wenig wie möglich daraus zu machen (z. B. sicherstellen, dass eine Fusion stattfindet, versuchen Sie es -funbox-strict
). Versuchen Sie vielleicht, eine Streaming-Bibliothek (iostreams, Pipes, Conduit, Streaming) zu verwenden und in performGC
häufigeren Abständen direkt aufzurufen .
MutableByteArray
; GC ist in diesem Fall überhaupt nicht beteiligt)