Chúng tôi đang phát triển một chương trình nhận và chuyển tiếp "tin nhắn", trong khi vẫn giữ lịch sử tạm thời của những tin nhắn đó, để nó có thể cho bạn biết lịch sử tin nhắn nếu được yêu cầu. Tin nhắn được xác định bằng số, thường có kích thước khoảng 1 kilobyte và chúng tôi cần giữ hàng trăm ngàn tin nhắn này.
Chúng tôi muốn tối ưu hóa chương trình này cho độ trễ: thời gian giữa gửi và nhận tin nhắn phải dưới 10 mili giây.
Chương trình được viết bằng Haskell và được biên dịch bằng GHC. Tuy nhiên, chúng tôi đã phát hiện ra rằng việc tạm dừng thu gom rác quá dài đối với các yêu cầu về độ trễ của chúng tôi: hơn 100 mili giây trong chương trình thế giới thực của chúng tôi.
Chương trình sau đây là phiên bản đơn giản hóa của ứng dụng của chúng tôi. Nó sử dụng một Data.Map.Strict
để lưu trữ tin nhắn. Tin nhắn được ByteString
xác định bởi một Int
. 1.000.000 tin nhắn được chèn theo thứ tự số tăng dần và các tin nhắn cũ nhất sẽ liên tục bị xóa để giữ lịch sử ở mức tối đa 200.000 tin nhắn.
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])
Chúng tôi đã biên dịch và chạy chương trình này bằng cách sử dụng:
$ 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
Số liệu quan trọng ở đây là "tạm dừng tối đa" 0,0515 giây, hoặc 51 mili giây. Chúng tôi muốn giảm điều này ít nhất là một thứ tự cường độ.
Thử nghiệm cho thấy độ dài của tạm dừng GC được xác định bởi số lượng tin nhắn trong lịch sử. Mối quan hệ gần như tuyến tính, hoặc có lẽ siêu tuyến tính. Bảng dưới đây cho thấy mối quan hệ này. ( Bạn có thể xem các bài kiểm tra điểm chuẩn của chúng tôi ở đây và một số biểu đồ ở đây .)
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
Chúng tôi đã thử nghiệm với một số biến số khác để tìm hiểu xem liệu chúng có thể giảm độ trễ này hay không, không biến nào tạo ra sự khác biệt lớn. Trong số các biến không quan trọng này là: tối ưu hóa ( -O
, -O2
); Tùy chọn RTS GC ( -G
, -H
, -A
, -c
), số lượng lõi ( -N
), cấu trúc dữ liệu khác nhau ( Data.Sequence
), kích thước của bài viết, và số lượng được tạo ra rác ngắn ngủi. Yếu tố quyết định áp đảo là số lượng tin nhắn trong lịch sử.
Lý thuyết làm việc của chúng tôi là các tạm dừng là tuyến tính trong số lượng tin nhắn vì mỗi chu kỳ GC phải đi qua tất cả bộ nhớ có thể truy cập được và sao chép nó, đó là các hoạt động tuyến tính rõ ràng.
Câu hỏi:
- Liệu lý thuyết thời gian tuyến tính này có đúng không? Độ dài của tạm dừng GC có thể được thể hiện theo cách đơn giản này, hoặc thực tế phức tạp hơn?
- Nếu tạm dừng GC là tuyến tính trong bộ nhớ làm việc, có cách nào để giảm các yếu tố liên tục không?
- Có bất kỳ lựa chọn cho GC gia tăng, hoặc bất cứ điều gì giống như nó? Chúng tôi chỉ có thể xem các tài liệu nghiên cứu. Chúng tôi rất sẵn sàng giao dịch thông lượng cho độ trễ thấp hơn.
- Có cách nào để "phân vùng" bộ nhớ cho các chu trình GC nhỏ hơn, ngoài việc chia thành nhiều quy trình không?
COntrol.Concurrent.Chan
? Các đối tượng có thể thay đổi thay đổi phương trình)? Tôi sẽ đề nghị bắt đầu bằng cách đảm bảo bạn biết loại rác nào bạn tạo ra và tạo ra càng ít càng tốt (ví dụ: đảm bảo sự hợp nhất xảy ra, hãy thử -funbox-strict
). Có thể thử sử dụng một lib phát trực tuyến (iostreams, ống dẫn, ống dẫn, phát trực tuyến) và gọi performGC
trực tiếp trong khoảng thời gian thường xuyên hơn.
MutableByteArray
; GC sẽ không liên quan gì đến trường hợp đó)