Lấy giờ là thứ mọi log, mọi số liệu, mọi phép đo đều làm. Bài này đo xem nó tốn bao nhiêu — và tìm ra một chênh lệch 11 lần giữa hai cách gọi cùng một hàm.

Chi phí và độ phân giải của từng đồng hồ

Bảng

Ba triệu lần gọi mỗi loại:

Cách lấy ns mỗi lần Độ phân giải thật
Vòng lặp rỗng (mốc so sánh) 0,62
CLOCK_REALTIME_COARSE 2,27 1 ms
CLOCK_MONOTONIC_COARSE 2,36 1 ms
time() 3,27 1 giây
CLOCK_REALTIME (qua vDSO) 13,14 41 ns
CLOCK_MONOTONIC (qua vDSO) 13,59 41 ns
gettimeofday() 14,22 1 µs
syscall() trực tiếp, bỏ qua vDSO 154,77 41 ns
CLOCK_THREAD_CPUTIME_ID 158,95

Cùng một con số, hai cái giá cách nhau 11 lần

Dòng CLOCK_MONOTONIC và dòng syscall() trực tiếp lấy đúng cùng một giá trị từ đúng cùng một nguồn. Chênh nhau 11,4 lần.

Khác biệt là vDSO — virtual dynamic shared object.

Không có vDSO: ứng dụng gọi hệ thống, CPU đổi sang chế độ nhân, nhân đọc đồng hồ, đổi chế độ quay ra. Đó là khoảng 155 ns, và nó khớp với con số ~1.000 ns mà phần 24 đo cho một lời gọi hệ thống nặng hơn.

Có vDSO: nhân ánh xạ một trang chỉ đọc vào không gian địa chỉ của mọi tiến trình, và cập nhật giờ vào trang đó. Ứng dụng đọc thẳng — không có lời gọi hệ thống nào cả.

grep vdso /proc/self/maps
# ffffad503000-ffffad505000 r-xp 00000000 00:00 0    [vdso]

Dòng đó có mặt trong mọi tiến trình Linux. Bốn hàm được phục vụ qua vDSO: clock_gettime, gettimeofday, time, getcpu.

Hệ quả thực dụng: gọi clock_gettime() qua thư viện chuẩn, đừng bao giờ gọi syscall() thẳng. Một số thư viện và runtime cũ làm điều thứ hai để tránh phụ thuộc libc, và trả giá 11 lần.

Với seccomp chặn quá tay hoặc trong một số môi trường ảo hoá, vDSO bị tắt và mọi lần lấy giờ rơi về đường chậm — một dạng suy giảm hiệu năng khó lần ra vì mã nguồn không đổi.

COARSE rẻ hơn 6 lần

CLOCK_REALTIME_COARSE tốn 2,27 ns so với 13,14 ns.

Nó không đọc bộ đếm phần cứng. Nó đọc giá trị mà nhân đã ghi vào trang vDSO ở lần ngắt hẹn giờ gần nhất — nên nó chỉ là một lần đọc bộ nhớ, không có phép tính nào.

Đổi lại là độ phân giải 1 ms.

Khi nào dùng được:

Dùng Không dùng
Đóng dấu thời gian cho log Đo thời gian một thao tác
Kiểm tra thời hạn bộ đệm Đo độ trễ
Giới hạn tần suất theo cửa sổ giây Bất cứ thứ gì dưới vài mili giây

Với một dịch vụ ghi 10.000 dòng log mỗi giây, đổi sang COARSE tiết kiệm khoảng 0,1 ms mỗi giây — không đáng. Với một vòng lặp gọi giờ một triệu lần mỗi giây, nó tiết kiệm 11 ms mỗi giây, và lúc đó đáng.

clock_getres() nói dối

CLOCK_MONOTONIC          clock_getres báo 1.000.000 ns | bước nhỏ nhất đo được       41 ns
CLOCK_REALTIME           clock_getres báo 1.000.000 ns | bước nhỏ nhất đo được       41 ns
CLOCK_MONOTONIC_COARSE   clock_getres báo 1.000.000 ns | bước nhỏ nhất đo được 1.000.000 ns
CLOCK_REALTIME_COARSE    clock_getres báo 1.000.000 ns | bước nhỏ nhất đo được 1.000.000 ns

Cả bốn đồng hồ đều báo cùng một con số: 1 ms. Với hai đồng hồ COARSE thì đúng. Với hai đồng hồ thường thì sai 24.000 lần.

clock_getres() trả về độ mịn của bộ hẹn giờ của nhân — 1 ms tương ứng CONFIG_HZ=1000 — chứ không phải độ phân giải của đồng hồ mà bạn hỏi.

Cách đúng để biết độ phân giải thật là đo: gọi liên tục và tìm bước nhảy nhỏ nhất khác 0. Đó là điều tôi làm để ra con số 41 ns.

Nếu bạn đang viết mã dựa vào clock_getres() để quyết định có đo được khoảng thời gian ngắn hay không, kết quả sẽ là bỏ qua những phép đo hoàn toàn khả thi.

Nguồn đồng hồ của phần cứng

cat /sys/devices/system/clocksource/clocksource0/current_clocksource
cat /sys/devices/system/clocksource/clocksource0/available_clocksource

Máy tôi đo (ARM) chỉ có arch_sys_counter. Trên x86 thường thấy:

Nguồn Đặc điểm
tsc nhanh nhất, đọc thẳng thanh ghi CPU
hpet chậm hơn nhiều, phải qua bus
acpi_pm chậm nhất
kvm-clock dành cho máy ảo

Nếu current_clocksourcehpet trên một máy chủ hiện đại, đó là dấu hiệu có vấn đề. Nhân chỉ rơi về hpet khi nó phát hiện tsc không ổn định — thường do BIOS cũ hoặc do di chuyển máy ảo giữa các máy chủ vật lý. Hệ quả là mọi lần lấy giờ đắt lên hàng chục lần, và vDSO có thể bị vô hiệu hoá hoàn toàn.

Kiểm tra trong nhật ký nhân:

dmesg | grep -iE 'clocksource|tsc'

CLOCK_THREAD_CPUTIME_ID không có trong vDSO

158,95 ns, ngang với một lời gọi hệ thống thật — vì nó một lời gọi hệ thống. Nhân phải cộng thời gian CPU của luồng, và con số đó không nằm sẵn trong trang chia sẻ.

Đây là đồng hồ mà mọi trình đo thời gian CPU dùng. Nếu mã của bạn đo CLOCK_THREAD_CPUTIME_ID quanh những đoạn ngắn, chi phí đo có thể lớn hơn thứ được đo.

Chọn đồng hồ nào

Mục đích Đồng hồ Vì sao
Đo khoảng thời gian CLOCK_MONOTONIC không nhảy khi NTP chỉnh giờ
Dấu thời gian để hiển thị, ghi log CLOCK_REALTIME là giờ thật
Log tần suất rất cao CLOCK_REALTIME_COARSE rẻ 6 lần, 1 ms là đủ
Đo CPU của luồng CLOCK_THREAD_CPUTIME_ID đắt, dùng tiết kiệm
Hết hạn, hẹn giờ CLOCK_MONOTONIC tuyệt đối không dùng REALTIME

Dòng cuối là lỗi kinh điển: dùng CLOCK_REALTIME cho thời hạn. Khi NTP chỉnh đồng hồ lùi lại, mọi thời hạn đang chờ đột ngột lùi theo — và khi chỉnh tới, chúng hết hạn cùng lúc.

Thử ba mươi giây

cat > /tmp/t.c <<'EOF'
#include <stdio.h>
#include <time.h>
static double now(void){struct timespec t;clock_gettime(CLOCK_MONOTONIC,&t);
  return t.tv_sec+t.tv_nsec/1e9;}
int main(void){
  struct timespec ts; long N=2000000; volatile long s=0;
  double t=now(); for(long i=0;i<N;i++){clock_gettime(CLOCK_MONOTONIC,&ts);s+=ts.tv_nsec;}
  printf("MONOTONIC        : %6.2f ns\n",(now()-t)/N*1e9);
  t=now(); for(long i=0;i<N;i++){clock_gettime(CLOCK_MONOTONIC_COARSE,&ts);s+=ts.tv_nsec;}
  printf("MONOTONIC_COARSE : %6.2f ns  (s=%ld)\n",(now()-t)/N*1e9,s);
  return 0;
}
EOF
cc -O2 -o /tmp/t /tmp/t.c && /tmp/t

echo "--- nguon dong ho ---"
cat /sys/devices/system/clocksource/clocksource0/current_clocksource
grep -c vdso /proc/self/maps

Nếu dòng đầu vượt 100 ns, vDSO của bạn không hoạt động hoặc nguồn đồng hồ đã rơi về đường chậm — và mọi thứ trong hệ thống có lấy giờ đang trả giá đó.

Phần sau: bẫy khi đo — những sai lầm tôi đã mắc trong bốn mươi hai phần trước.