Bốn mươi lăm phần, khoảng bốn mươi tư phép đo thật trong một container Linux — từ chi phí một lời gọi hệ thống, qua tiến trình và luồng, bộ nhớ ảo và page fault, giao tiếp liên tiến trình, lập lịch và real-time, giới hạn tài nguyên, lưu trữ và I/O, các chỉ số bộ nhớ, tới chính phương pháp đo. Sê-ri này không dạy bạn danh sách các khái niệm hệ điều hành — sách giáo khoa làm việc đó tốt hơn. Nó dạy một thói quen: khi bạn tự hỏi "cái này tốn bao nhiêu?", đừng đoán — hãy đo. Và gần như mỗi lần đo, trực giác đều sai theo một cách đáng nhớ. Bài kết này gom lại thành một bức tranh, với một phép đo cuối cùng làm xương sống.

Hệ điều hành: đo, đừng đoán

Thang độ trễ: năm bậc độ lớn

Tôi đo lại năm con số neo của cả sê-ri trong một chương trình, xếp thành một thang độ trễ:

THANG ĐỘ TRỄ (đo lại, ns):
  đọc RAM tuần tự        :     0,25 ns   (hardware prefetch theo kịp — phần 41)
  gọi hàm user-space     :     0,71 ns   (một call/ret — phần 30/36)
  syscall (getpid)       :   100,45 ns   (vượt ranh giới user/kernel — phần 1/36)
  đọc RAM ngẫu nhiên     :   107,86 ns   (một cache miss — phần 37/41)
  đánh thức (ngủ -> thức):  ~8.965   ns   (một chuyển ngữ cảnh — phần 5/27)

Từ rẻ nhất tới đắt nhất là ~36.000 lần — năm bậc độ lớn, tất cả trên cùng một máy. Cái thang này tự nó dạy ba điều mà không lý thuyết nào nói rõ bằng con số:

Một cache miss đắt xấp xỉ một lời gọi hệ thống. Đọc RAM ngẫu nhiên (108 ns) gần bằng một syscall vượt vào nhân (100 ns). Nghĩa là một lần "trượt cache" trong code của bạn — thứ trông vô hại — tốn tương đương một lần gọi vào hệ điều hành. Đây là lý do bố cục dữ liệu (mảng liền, mẫu truy cập đoán được) quan trọng ngang việc giảm syscall.

"Ngủ rồi thức" là thao tác thường-gặp đắt nhất. Đánh thức một luồng (~9 µs) đắt gấp ~90 lần một syscall, ~36.000 lần một lần đọc RAM tuần tự. Mọi thứ liên quan tới chặn và đánh thức — mutex bị giành, biến điều kiện, semaphore phải ngủ, chuyển ngữ cảnh bắt buộc — đều chia chung cái sàn ~9 µs này. Đó là vì sao "tránh phải ngủ" (lock-free, batching, không oversubscribe) là đòn bẩy hiệu năng lớn nhất trong đồng thời.

Tuần tự đánh bại ngẫu nhiên ~430 lần trên cùng một RAM. 0,25 ns so với 108 ns — cùng bộ nhớ, chỉ khác mẫu truy cập. "Random Access Memory" truy cập được ngẫu nhiên nhưng không đều giá.

Những "đo hớ" lặp lại: bốn mẫu

Đọc kỹ 44 phép đo, có bốn mẫu trực giác-sai quay đi quay lại — bốn cách "đo hớ" mà nếu nhớ, bạn sẽ tránh được phần lớn hiểu lầm về hiệu năng hệ thống:

1. Chi phí thường là per-thao-tác, không per-byte. Mỗi lần vượt ranh giới user/kernel ~100 ns, mỗi lần open một file ~1 µs, mỗi lần bàn giao qua hàng đợi. Nên ghi từng byte chậm hơn ghi cả buffer hàng chục nghìn lần dù cùng số byte; đọc 20.000 file nhỏ chậm hơn một file lớn dù cùng dung lượng. Thuốc luôn là gộp lô (batch) — trả cái phí cố định đó một lần.

2. Cái đắt thật là "phải ngủ / vượt ranh giới", không phải bản thân nguyên hàm. Semaphore/mutex khi không tranh chỉ ~vài ns (atomic trong user-space); chúng chỉ đắt (~9 µs) khi phải cho tiến trình ngủ. malloc không tự đắt — nó chỉ đắt khi phải mọc heap qua syscall. Đo cái nguyên hàm mà không hỏi "nó có phải chạm nhân/ngủ không" là gán chi phí nhầm chỗ.

3. Cấp phát lười ở khắp nơi. malloc/mmap chỉ đặt chỗ không gian ảo (VSZ); RAM thật (RSS) chỉ tốn khi bạn chạm — nên overcommit cho phép xin nhiều hơn RAM có, và malloc != NULL không đảm bảo bộ nhớ. Đây là lý do VSZ ≠ bộ nhớ dùng, và vì sao RSS chỉ bò lên khi bạn thật sự chạm trang.

4. Đoán được hơn nhanh thô. Prefetch/readahead thưởng cho mẫu truy cập đều và trừng phạt ngẫu nhiên; real-time không làm CPU nhanh hơn mà cho độ trễ tất định (jitter thấp). Nhiều tối ưu hệ thống là làm cho hành vi đoán được, không phải làm phép tính nhanh hơn.

Và trên hết: đo cho đúng

Cả bốn mẫu trên chỉ lộ ra vì đo đúng cách — bài học của phần 44, nền của mọi con số trong sê-ri: cô lập một biến, warmup, lặp nhiều lần và lấy min (nhiễu chỉ thêm thời gian, lần nhanh nhất là sạch nhất), chống trình biên dịch tối ưu mất phép đo, và báo trung thực khi môi trường không cho thấy hiệu ứng. Sê-ri này có những bài mà tôi không đo được điều mình muốn — NUMA phần 37 (máy một node), seek đĩa vật lý (file trong cache), throughput của sendfile trên loopback — và tôi nói thẳng ra thay vì bịa. Một phép đo trung thực báo "không quan sát được ở đây" đáng giá hơn một con số đẹp nhưng sai.

Bài học đo lường cuối, gói cả sê-ri: đo, đừng đoán — vì trực giác về chi phí hệ thống sai một cách có hệ thống, và chỉ con số đo đúng cách mới sửa được. Con số mang theo: một thang độ trễ trải ~36.000 lần (đọc RAM tuần tự 0,25 ns; gọi hàm 0,71 ns; syscall 100 ns xấp xỉ một cache miss 108 ns; đánh thức ngủ→thức ~9 µs); bốn mẫu đo hớ lặp lại — chi phí per-thao-tác không per-byte (gộp lô), cái đắt là phải ngủ/vượt ranh giới không phải nguyên hàm, cấp phát lười (VSZ ảo vs RSS khi chạm), và đoán được hơn nhanh thô (prefetch, real-time); tất cả chỉ đáng tin nhờ đo đúng cách: cô lập biến, warmup, lấy min, trung thực.

Thử ba mươi giây

Chọn một câu hỏi hiệu năng bạn đang đoán câu trả lời — "gọi hàm này có đắt không?", "nhiều luồng có nhanh hơn không?", "cache có giúp không?" — và thay vì đoán, viết mười dòng đo nó: một vòng lặp, clock_gettime trước và sau, warmup, lặp, lấy min. Bạn sẽ ngạc nhiên bao nhiêu lần con số thật khác điều bạn tưởng — có khi gấp trăm lần, có khi bằng nhau khi bạn nghĩ khác xa. Đó là toàn bộ tinh thần của sê-ri này, gói trong ba mươi giây: giữa bạn và câu trả lời đúng thường chỉ là mười dòng code đo — và thói quen bấm giờ thay vì phỏng đoán là thứ tách một lập trình viên biết hệ thống của mình với một người tưởng mình biết. Đo, đừng đoán.