bài về nguồn đồng hồ ta thấy đọc giờ chính xác tới nano giây. Vậy khi ta bảo hệ điều hành "ngủ 1 micro giây", nó có ngủ đúng 1 micro giây không? Câu hỏi tưởng vô thưởng vô phạt này ẩn một sự thật mà rất nhiều người viết code định thời (rate limiter, retry backoff, game loop, benchmark) hiểu sai — dẫn tới những vòng lặp "ngủ 1ms" bỗng chạy chậm hơn cả chục lần dự tính. Bài này đo một lệnh sleep ngắn thực sự ngủ bao lâu, và phát hiện con số "độ phân giải" mà API tự khai không phải con số ta thật sự nhận được.

Độ phân giải timer

Xin ngủ ngắn, ngủ bao lâu thật?

Khi gọi nanosleep, usleep, hay sleep 0.001, ta yêu cầu nhân treo tiến trình lại một khoảng thời gian rồi đánh thức. Trực giác nói: xin ngủ t thì ngủ đúng t. Nhưng có một chuỗi việc phải xảy ra sau khi hết giờ: timer của nhân phải báo (fire), rồi bộ lập lịch phải chọn tiến trình vừa được đánh thức và xếp nó trở lại lên một CPU — và nếu CPU đang ngủ (idle), phải đánh thức chính CPU đó trước. Toàn bộ chuỗi này tốn thời gian, tạo ra một sàn (floor): dù xin ngủ ngắn tới đâu, ta cũng không thể ngủ ít hơn cái sàn ấy.

Có một con số dễ gây nhầm: clock_getres trả về "độ phân giải" của đồng hồ. Trên máy tôi nó báo 1 nano giây — nghe như ta có thể định thời tới từng nano giây. Nhưng đó là độ phân giải đọc đồng hồ (đọc giờ mịn tới đâu), hoàn toàn khác với ngưỡng ngủ/đánh thức thực tế. Tôi muốn đo cái ngưỡng thật đó.

Đo: xin 1 micro giây, ngủ 84 micro giây

Tôi gọi nanosleep xin ngủ các mốc từ 1 nano giây tới 10 mili giây, mỗi mốc 200 lần, đo thời gian ngủ thật bằng CLOCK_MONOTONIC rồi lấy trung vị:

Xin ngủ Ngủ thật (trung vị) Gấp mấy lần
1 ns ~83 µs (chạm sàn)
1 µs ~84 µs ~84 lần
10 µs ~98 µs ~10 lần
100 µs ~233 µs ~2,3 lần
1 ms ~1.290 µs ~1,3 lần
10 ms ~13.600 µs ~1,36 lần

Con số phơi bày cái sàn rất rõ. Xin ngủ 1 nano giây, hay 1 micro giây, hay thậm chí 0 — kết quả đều là ngủ khoảng 80–84 micro giây. Không thể ngủ ngắn hơn thế. Xin 1 micro giây mà ngủ 84 micro giây nghĩa là gấp 84 lần so với yêu cầu. Càng xin ngắn, tỉ lệ vượt càng khủng khiếp; chỉ khi xin đủ lớn (1 ms, 10 ms) thì cái sàn ~80 µs mới trở thành một phần nhỏ và độ chính xác tương đối mới tốt lên — dù vẫn vượt 30–36% và còn dao động.

Một lần tôi đo hớ: độ phân giải "tự khai" không phải độ chính xác thật

Tôi vào bài với một suy luận nghe rất chặt chẽ: clock_getres báo độ phân giải 1 nano giây, vậy hệ thống định thời được tới nano giây, và nanosleep(1 µs) chắc sẽ ngủ cỡ 1 micro giây. Con số "1 ns" là bằng chứng cứng, tôi tin nó. Nhưng phép đo bác bỏ thẳng thừng: nanosleep(1 µs) ngủ 84 micro giây — dài gấp 84 lần điều "1 ns" gợi ý.

Sai lầm của tôi là lẫn hai loại độ chính xác. clock_getres nói về việc đọc đồng hồ mịn tới đâu — và đúng, ta đọc được giờ tới nano giây. Nhưng nó không nói gì về việc ngủ rồi được đánh thức chính xác tới đâu. Cái chặn một giấc ngủ ngắn không phải độ mịn của timer, mà là độ trễ đánh thức — chi phí của bộ lập lịch để xếp tiến trình vừa hết giờ trở lại lên CPU. Timer báo đúng giờ (tới nano giây), nhưng giữa lúc nó báo và lúc tiến trình của tôi thật sự chạy lại có một khoảng ~80 micro giây không thể tránh.

Tôi khẳng định được đây là chi phí lập lịch chứ không phải một nhịp timer cứng bằng một quan sát nữa: cái sàn thay đổi theo trạng thái máy. Khi tôi chạy phép đo trên máy đang bận (nhiều tiến trình khác), trung vị cho mốc 1 micro giây tụt xuống còn ~58 micro giây — ngắn hơn lúc máy rảnh. Nghe ngược đời, nhưng có lý: khi máy rảnh, CPU rơi vào trạng thái ngủ tiết kiệm điện và phải mất thời gian để đánh thức; khi máy bận, CPU luôn thức nên đánh thức tiến trình nhanh hơn. Cái sàn là một hiện tượng lập lịch động, không phải một hằng số phần cứng. Bài học đo lường: đừng lẫn độ chính xác một công cụ tự khai (clock_getres = 1 ns) với độ chính xác ta thật sự nhận được (~80 µs) — con số quảng cáo và con số trải nghiệm là hai đại lượng khác nhau, và chỉ phép đo mới cho biết cái sau.

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

Hệ quả đầu tiên là đừng thiết kế vòng lặp dựa trên sleep ngắn. Một vòng "ngủ 1 ms mỗi lần" tưởng chạy 1000 vòng/giây, nhưng vì mỗi lần ngủ thật ~1,3 ms (và có thể hơn khi tải cao), nó chỉ chạy ~750 vòng/giây — sai một phần tư. Vòng "ngủ 100 µs" còn tệ hơn, chạy chưa tới một nửa tốc độ mong đợi vì cái sàn ~80 µs nuốt mất phần lớn. Nếu cần nhịp đều và chính xác, đừng cộng dồn các sleep ngắn (sai số tích lũy), mà tính deadline tuyệt đối bằng đồng hồ monotonic rồi ngủ tới mốc đó (clock_nanosleep với TIMER_ABSTIME).

Hệ quả thứ hai là muốn chờ ngắn hơn cái sàn thì phải quay bận (busy-wait), đổi CPU lấy độ chính xác. Nếu bạn thật sự cần chờ đúng 5 micro giây (ví dụ định thời phần cứng, spinlock có giới hạn), sleep là vô dụng — nó sẽ ngủ ~80 µs. Cách duy nhất là quay bận: lặp đọc đồng hồ monotonic cho tới khi đủ giờ, chấp nhận đốt CPU. Đây là đánh đổi kinh điển: sleep nhường CPU nhưng có sàn thô ~80 µs; busy-wait chính xác tới micro giây nhưng ngốn 100% một nhân. Chọn theo việc bạn cần độ chính xác hay tiết kiệm CPU.

Hệ quả thứ ba, về đo lường: luôn phân biệt "độ phân giải công cụ khai" với "độ chính xác thực nghiệm". Con số mang theo: clock_getres báo 1 ns nhưng một lệnh sleep có sàn ~80 µs do độ trễ đánh thức của bộ lập lịch (không phải độ mịn timer) — nên xin ngủ 1 µs thì ngủ thật ~84 µs (gấp 84 lần), và chỉ khi xin cỡ mili giây độ chính xác tương đối mới khá; muốn chờ ngắn hơn sàn phải busy-wait. Một API khai độ phân giải nano giây không hứa hẹn gì về độ trễ thực tế; chỉ có đo mới biết bạn thật sự chờ được ngắn tới đâu.

Thử ba mươi giây

Đo cái sàn trên máy bạn không cần viết code C: chạy for i in $(seq 1 1000); do :; done để làm nóng, rồi thử python3 -c "import time; s=time.monotonic(); time.sleep(0.0001); print((time.monotonic()-s)*1e6, 'us')" — bạn xin ngủ 100 micro giây và sẽ thấy con số thật lớn hơn nhiều (thường 150–300 µs). Đổi 0.0001 thành 0.000001 (1 µs): con số gần như không đổi, vì bạn đã chạm sàn — xin ngắn hơn cũng vô ích. Muốn thấy độ phân giải "tự khai", trong C gọi clock_getres(CLOCK_MONOTONIC, &r) và in r.tv_nsec — nó sẽ báo 1 (nano giây), một con số đẹp đẽ chẳng liên quan gì tới ~80 micro giây bạn vừa đo được. Khoảng cách giữa hai con số đó chính là điều bài này đo.