Мы разрабатываем программу, которая получает и пересылает "сообщения", сохраняя при этом временную историю этих сообщений, чтобы она могла сообщать вам историю сообщений по запросу. Сообщения идентифицируются численно, обычно размером около 1 килобайта, и нам нужно хранить сотни тысяч этих сообщений.
Мы хотим оптимизировать эту программу для задержки: время между отправкой и получением сообщения должно быть ниже 10 миллисекунд.
Программа написана в Haskell и скомпилирована с GHC. Однако мы обнаружили, что паузы в сборке мусора слишком велики для наших требований к задержкам: более 100 миллисекунд в нашей реальной программе.
Следующая программа представляет собой упрощенную версию нашего приложения. Для хранения сообщений используется Data.Map.Strict
. Сообщения ByteString
идентифицируются Int
. 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 должен пройти через всю доступную доступную память и скопировать его, что является явно линейными операциями.
Вопросы:
- Правильно ли эта теория линейного времени? Можно ли выразить длину пауз GC таким простым способом, или же реальность более сложна?
- Если GC-пауза является линейной в рабочей памяти, есть ли способ уменьшить постоянные факторы?
- Есть ли какие-либо опции для инкрементного GC или что-то в этом роде? Мы можем видеть только научные работы. Мы очень хотим торговать пропускной способностью для более низкой задержки.
- Есть ли способ "разбить" память на меньшие циклы GC, кроме разделения на несколько процессов?