Kami sedang mengembangkan program yang menerima dan meneruskan "pesan", sambil menyimpan riwayat sementara pesan-pesan itu, sehingga dapat memberi tahu Anda riwayat pesan jika diminta. Pesan diidentifikasi secara numerik, biasanya berukuran sekitar 1 kilobyte, dan kita perlu menyimpan ratusan ribu pesan ini.
Kami ingin mengoptimalkan program ini untuk latensi: waktu antara mengirim dan menerima pesan harus di bawah 10 milidetik.
Program ini ditulis dalam Haskell dan dikompilasi dengan GHC. Namun, kami telah menemukan bahwa jeda pengumpulan sampah terlalu lama untuk persyaratan latensi kami: lebih dari 100 milidetik dalam program dunia nyata kami.
Program berikut adalah versi sederhana dari aplikasi kami. Ini menggunakan Data.Map.Strict
untuk menyimpan pesan. Pesan ByteString
diidentifikasi oleh seorang Int
. 1.000.000 pesan disisipkan dalam urutan numerik yang meningkat, dan pesan terlama terus dihapus untuk menjaga riwayat maksimum 200.000 pesan.
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])
Kami mengkompilasi dan menjalankan program ini menggunakan:
$ 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
Metrik penting di sini adalah "jeda maksimum" dari 0,0515 detik, atau 51 milidetik. Kami ingin mengurangi ini setidaknya dengan urutan besarnya.
Eksperimen menunjukkan bahwa panjang jeda GC ditentukan oleh jumlah pesan dalam riwayat. Hubungannya kira-kira linear, atau mungkin super-linear. Tabel berikut menunjukkan hubungan ini. ( Anda dapat melihat tes pembandingan kami di sini , dan beberapa grafik di sini .)
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
Kami telah bereksperimen dengan beberapa variabel lain untuk menemukan apakah mereka dapat mengurangi latensi ini, tidak ada yang membuat perbedaan besar. Di antara variabel-variabel yang tidak penting adalah: optimasi ( -O
, -O2
); Pilihan RTS GC ( -G
, -H
, -A
, -c
), jumlah core ( -N
), struktur data yang berbeda ( Data.Sequence
), ukuran pesan, dan jumlah yang dihasilkan sampah berumur pendek. Faktor penentu yang luar biasa adalah jumlah pesan dalam sejarah.
Teori kerja kami adalah bahwa jeda linear dalam jumlah pesan karena setiap siklus GC harus menelusuri semua memori yang dapat diakses dan menyalinnya, yang jelas merupakan operasi linear.
Pertanyaan:
- Apakah teori waktu linear ini benar? Bisakah panjang GC jeda diungkapkan dengan cara sederhana ini, atau apakah kenyataannya lebih kompleks?
- Jika jeda GC linier dalam memori kerja, apakah ada cara untuk mengurangi faktor konstan yang terlibat?
- Apakah ada opsi untuk GC tambahan, atau yang seperti itu? Kami hanya bisa melihat makalah penelitian. Kami sangat ingin memperdagangkan throughput untuk latensi yang lebih rendah.
- Apakah ada cara untuk "mempartisi" memori untuk siklus GC yang lebih kecil, selain membelah menjadi beberapa proses?
COntrol.Concurrent.Chan
misalnya? Objek yang dapat berubah mengubah persamaan)? Saya sarankan mulai dengan memastikan Anda tahu sampah apa yang Anda hasilkan dan sesedikit mungkin membuat sampah (mis. Pastikan fusi terjadi, coba -funbox-strict
). Mungkin mencoba menggunakan lib streaming (iostreams, pipa, saluran, streaming), dan menelepon performGC
langsung pada interval yang lebih sering.
MutableByteArray
; GC tidak akan terlibat sama sekali dalam hal ini)