Suốt sê-ri này mọi phép đo đều bắt đầu bằng một lời gọi clock_gettime để lấy giờ. Ở phần đầu ta thấy clock_gettime rẻ bất ngờ (~12,7 ns) nhờ vDSO. Nhưng clock_gettime không phải một thứ — nó nhận một clockid chọn nguồn thời gian, và các nguồn khác nhau rất nhiều về cả chi phí lẫn ý nghĩa. Chọn nhầm clock có thể làm chương trình chậm, hoặc tệ hơn, cho sai kết quả đo thời lượng. Tôi đo các clock chính trong container gcc:13 (ARM) — cả tốc độ lẫn độ phân giải.

clock_gettime và nguồn thời gian

Nhiều clock, khác cả giá lẫn nghĩa

clock_gettime(clockid, &ts) đọc thời gian, nhưng clockid chọn nguồn nào:

  • CLOCK_MONOTONIC / CLOCK_REALTIME: thời gian tường (wall clock). Được phơi bày qua vDSO nên đọc không vào nhân — rẻ và phân giải mịn.
  • CLOCK_MONOTONIC_COARSE: một bản "thô" của MONOTONIC. Nhân cập nhật một biến thời gian định kỳ (mỗi nhịp lịch ~1 ms) và đọc COARSE chỉ là đọc biến đó — rẻ hơn nữa, nhưng phân giải chỉ ~1 ms.
  • CLOCK_PROCESS_CPUTIME_ID / CLOCK_THREAD_CPUTIME_ID: thời gian CPU mà tiến trình/luồng đã dùng (khác thời gian tường). Tính chúng cần nhân cộng dồn thống kê lập lịch — phải vào nhân, không qua vDSO.

Và một khác biệt ý nghĩa quan trọng: CLOCK_REALTIME có thể nhảy — khi NTP đồng bộ, admin đổi giờ, hay giây nhuận, nó có thể tiến vọt hoặc lùi. CLOCK_MONOTONIC không bao giờ lùi — nó chỉ tăng đều. Điều này quyết định clock nào dùng để đo thời lượng.

Đo: chênh 10 lần về giá, 25.000 lần về phân giải

clock                     | ns/lần | phân giải
CLOCK_MONOTONIC           | 13,5   | ~41 ns (vDSO, mịn)
CLOCK_REALTIME            | 13,0   | ~41 ns (vDSO, NHẢY được)
CLOCK_MONOTONIC_COARSE    |  3,2   | ~1 ms (thô)
CLOCK_PROCESS_CPUTIME_ID  | 143,7  | (vào nhân)
CLOCK_THREAD_CPUTIME_ID   | 140,9  | (vào nhân)

Chi phí trải một dải rộng. MONOTONIC/REALTIME ~13 ns — khớp con số vDSO ở phần 1, không vào nhân. COARSE còn rẻ hơn: 3,2 ns, gần 4 lần nhanh hơn MONOTONIC — vì nó chỉ đọc một giá trị nhân đã cập nhật sẵn, không cần đọc bộ đếm phần cứng. Nhưng đổi lại phân giải thô ~1 ms: nó chỉ đổi giá trị mỗi ~1 ms, nên bạn không thể đo bất cứ thứ gì ngắn hơn ~1 ms với nó.

Còn hai clock CPU-time (PROCESS/THREAD_CPUTIME) tốn ~140 ns — gấp ~11 lần MONOTONIC. Vì chúng không ở vDSO: tính thời gian CPU đã dùng cần vào nhân cộng dồn thống kê lập lịch. Nếu bạn gọi chúng trong một vòng nóng để "đo CPU của mình liên tục", bạn trả 140 ns mỗi lần — đáng kể. Đáng nói thêm về độ phân giải: clock_getres báo MONOTONIC là 1 ns, nhưng thực đo bước nhảy nhỏ nhất là ~41 ns — vì đó xấp xỉ thời gian thật trôi giữa hai lần đọc liên tiếp; con số nhân báo là lý thuyết, cái đo được mới là giới hạn thực.

Một lần tôi đo hớ: không phải clock nào cũng như nhau

Tôi vào đo với hai giả định. Thứ nhất: "lấy giờ là lấy giờ — clock nào cũng cùng giá". Sai — dải chi phí trải 10 lần (COARSE 3 ns tới CPU-time 140 ns). Thứ hai, ngầm và nguy hiểm hơn: "clock nào cũng đo thời lượng được, cứ trừ hai mốc". Sai — dùng CLOCK_REALTIME để đo thời lượng là một bug chực chờ: nếu NTP chỉnh giờ giữa hai mốc, hiệu số có thể âm hoặc lệch cả giây, cho một phép đo vô nghĩa mà không có lỗi rõ ràng.

Bài học đo lường: "lấy giờ" là nhiều thứ khác nhau — chọn nguồn theo cả tốc độ, độ phân giải, ngữ nghĩa (có nhảy hay không). Để đo thời lượng (dt = t2 − t1), luôn dùng CLOCK_MONOTONIC — nó không bao giờ lùi, nên hiệu số luôn dương và đúng. (Đây chính là lý do hàm now() trong mọi phép đo của cả sê-ri này dùng CLOCK_MONOTONIC, không phải REALTIME — đúng tinh thần đo lường vi mô đúng cách.) Dùng CLOCK_REALTIME chỉ khi bạn cần thời điểm lịch thật (ghi timestamp vào log, so với giờ thế giới) — và chấp nhận nó có thể nhảy. Còn chọn giữa MONOTONIC và COARSE thì theo độ phân giải cần: đo vi mô (dưới ms) phải MONOTONIC; nhưng nếu bạn chỉ cần biết "đã qua ~vài ms chưa" (timeout thô, rate limit, đóng dấu thời gian log tần suất cao), COARSE rẻ hơn 4 lần và đủ tốt.

Vì sao điều này quan trọng khi lập trình

Hệ quả đầu tiên: đo thời lượng luôn dùng CLOCK_MONOTONIC, không bao giờ CLOCK_REALTIME. Đây là một lỗi correctness thật: code đo "mất bao lâu" bằng gettimeofday/CLOCK_REALTIME sẽ chạy đúng gần như mọi lúc, rồi cho một con số âm hay sai lệch đúng lúc NTP chỉnh giờ hoặc đổi múi giờ — một bug hiếm, khó tái hiện, y như các bug đồng thời của sê-ri trước. Mọi thư viện đo thời gian nghiêm túc (benchmark, timeout) dùng MONOTONIC.

Hệ quả thứ hai: chọn clock theo độ phân giải cần, để tiết kiệm khi có thể. Nếu bạn đóng dấu thời gian tần suất rất cao và chỉ cần độ chính xác ~ms (log, rate limiting, cache TTL), CLOCK_MONOTONIC_COARSE rẻ hơn 4 lần — đáng kể ở hàng triệu lần gọi. Ngược lại, đừng dùng COARSE để đo những khoảng dưới ms: phân giải 1 ms của nó sẽ cho bạn 0 hoặc 1 ms, vô dụng. Và biết rằng đo thời gian CPU (CPUTIME clocks) đắt gấp ~11 lần thời gian tường — đừng gọi trong vòng nóng.

Hệ quả thứ ba là tinh thần đo lường: một API tưởng đơn giản ("lấy giờ") thực ra là nhiều lựa chọn với chi phí và ngữ nghĩa khác nhau — biết bạn đang chọn cái nào. Con số mang theo: clock_gettime khác nhau ~10 lần theo nguồn: MONOTONIC/REALTIME qua vDSO ~13 ns (phân giải mịn ~41 ns), MONOTONIC_COARSE ~3,2 ns (rẻ nhất nhưng phân giải THÔ ~1 ms, chỉ đọc giá trị nhân cập nhật sẵn), PROCESS/THREAD_CPUTIME ~140 ns (phải vào nhân, ~11 lần đắt); và CLOCK_REALTIME có thể NHẢY (NTP/đổi giờ) nên đo THỜI LƯỢNG phải dùng CLOCK_MONOTONIC (không bao giờ lùi), còn REALTIME chỉ để lấy thời điểm lịch. Lấy giờ không phải một thứ — chọn đúng nguồn.

Thử ba mươi giây

Tìm trong code chỗ bạn đo "mất bao lâu" — quanh một thao tác, một request, một vòng lặp. Nó dùng clock gì? Nếu là time(), gettimeofday(), hay clock_gettime(CLOCK_REALTIME, ...) để đo thời lượng, đó là một bug tiềm ẩn: một lần NTP chỉnh giờ giữa chừng và phép đo của bạn cho số âm hoặc nhảy vọt. Đổi sang CLOCK_MONOTONIC (hay std::chrono::steady_clock trong C++, time.monotonic() trong Python) — nó không bao giờ lùi. Rồi hỏi tiếp: nếu bạn lấy timestamp tần suất rất cao mà chỉ cần độ chính xác mili-giây, có đáng đổi sang COARSE để rẻ hơn 4 lần không? Ba mươi giây kiểm "clock nào cho việc gì" đó bắt được một lỗi correctness kín (REALTIME đo thời lượng) và một cơ hội tối ưu (COARSE cho timestamp thô) mà cái nhìn "lấy giờ là lấy giờ" bỏ lỡ.