Ми розробляємо програму, яка отримує та передає "повідомлення", зберігаючи тимчасову історію цих повідомлень, щоб вона могла повідомити вам історію повідомлень за запитом. Повідомлення ідентифікуються чисельно, зазвичай мають розмір близько 1 кілобайт, і нам потрібно зберігати сотні тисяч цих повідомлень.
Ми хочемо оптимізувати цю програму за затримкою: час між відправленням та отриманням повідомлення повинен бути менше 10 мілісекунд.
Програма написана в Haskell і складена з GHC. Однак ми виявили, що паузи з вивезенням сміття є занадто довгими для наших вимог щодо затримки: понад 100 мілісекунд в нашій реальній програмі.
Наступна програма - це спрощена версія нашої програми. Він використовує aData.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,0515s або 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, окрім розділення на кілька процесів?
COntrol.Concurrent.Chan
? Змінні об'єкти змінюють рівняння)? Я б запропонував розпочати, переконуючись, що ви знаєте, яке сміття ви генеруєте, і робите якомога менше його (наприклад, переконайтеся, що відбувається синтез, спробуйте -funbox-strict
). Можливо, спробуйте скористатися потоковою лінзою (іострими, труби, трубопровід, потокова передача) та дзвонити performGC
безпосередньо через більш часті проміжки часу.
MutableByteArray
; GC в цьому випадку взагалі не буде задіяний)