Stiamo sviluppando un programma che riceve e inoltra "messaggi", mantenendo una cronologia temporanea di tali messaggi, in modo che possa dirvi la cronologia dei messaggi, se richiesta. I messaggi sono identificati numericamente, hanno in genere circa 1 kilobyte di dimensione e dobbiamo conservare centinaia di migliaia di questi messaggi.Riduzione del tempo di pausa della raccolta dati obsoleti in un programma Haskell
Vogliamo ottimizzare questo programma per la latenza: il tempo tra l'invio e la ricezione di un messaggio deve essere inferiore a 10 millisecondi.
Il programma è scritto in Haskell e compilato con GHC. Tuttavia, abbiamo riscontrato che le pause nella raccolta dei rifiuti sono troppo lunghe per i nostri requisiti di latenza: oltre 100 millisecondi nel nostro programma reale.
Il seguente programma è una versione semplificata della nostra applicazione. Utilizza uno Data.Map.Strict
per archiviare i messaggi. I messaggi sono ByteString
s identificati da un Int
. 1.000.000 di messaggi vengono inseriti in ordine numerico crescente e i messaggi meno recenti vengono continuamente rimossi per mantenere la cronologia a un massimo di 200.000 messaggi.
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])
Abbiamo compilato ed eseguito questo programma utilizzando:
$ 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
L'importante qui è la metrica "pausa max" di 0.0515s, o 51 millisecondi. Vogliamo ridurre questo almeno di un ordine di grandezza.
La sperimentazione mostra che la durata di una pausa GC è determinata dal numero di messaggi nella cronologia. La relazione è approssimativamente lineare, o forse super-lineare. La seguente tabella mostra questa relazione. (You can see our benchmarking tests here, e some charts here.)
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
Abbiamo sperimentato diverse altre variabili per trovare se sono in grado di ridurre questa latenza, nessuno dei quali fanno una grande differenza. Tra queste variabili non importanti ci sono: ottimizzazione (-O
, -O2
); Opzioni RTS GC (-G
, -H
, -A
, -c
), numero di core (-N
), diverse strutture di dati (Data.Sequence
), la dimensione dei messaggi e la quantità di rifiuti generati di breve durata. Il fattore determinante travolgente è il numero di messaggi nella cronologia.
La nostra teoria di lavoro è che le pause sono lineari nel numero di messaggi, perché ogni ciclo GC deve camminare su tutta la memoria accessibile di lavoro e copiarlo, che sono chiaramente operazioni lineari.
Domande:
- È questa teoria lineare tempo corretto? La durata delle pause GC può essere espressa in questo modo semplice oppure la realtà è più complessa?
- Se GC pausa è lineare nella memoria di lavoro, esiste un modo per ridurre i fattori costanti coinvolti?
- Ci sono delle opzioni per GC incrementale, o qualcosa di simile? Possiamo vedere solo documenti di ricerca. Siamo molto disposti a scambiare il throughput per una latenza inferiore.
- Ci sono dei modi per la memoria "partizione" per i più piccoli, i cicli di GC, diverso da dividere in più processi?
Io purtroppo non può fornire alcun aiuto. Presumo che tu già [leggi questa domanda correlata] (http://stackoverflow.com/q/12404031/510937). Comunque avere un * must * richiesto per la latenza di essere al massimo di 10 millisecondi suona come un vincolo in tempo reale ... per raggiungere questo probabilmente si desidera ottimizzare lo scheduler del sistema operativo per le attività in tempo reale anche perché non ha senso ottimizzare un sacco il codice Haskell quando il sistema operativo decide che è possibile attendere 100 millisecondi ... – Bakuriu
@Bakuriu: giusto, ma 10 ms dovrebbero essere ottenibili con praticamente tutti i sistemi operativi moderni senza apportare modifiche. Quando eseguo programmi C semplicistici, anche sul mio vecchio Raspberry pi, ottengono facilmente latenze nell'intervallo di 5 ms, o almeno _reliably_ qualcosa come 15 ms. – leftaroundabout
Sei sicuro che il tuo test-case sia utile (ad esempio, non stai usando 'COntrol.Concurrent.Chan'? Gli oggetti mutabili cambiano l'equazione)? Ti suggerisco di iniziare assicurandoti di sapere quali rifiuti stai generando e facendo il meno possibile (ad esempio assicurati che la fusione avvenga, prova '-funbox-strict'). Forse provare a utilizzare una libreria di streaming (iostreams, pipe, conduit, streaming) e chiamare 'performGC' direttamente a intervalli più frequenti. – jberryman