Đây là bài cuối của sê-ri Hệ điều hành cho lập trình viên — 45 phần đo đủ thứ hệ điều hành làm với chương trình của bạn: lời gọi hệ thống, lập lịch, bộ nhớ ảo, lỗi trang, tiến trình, container. Nhưng thứ đọng lại quan trọng hơn từng con số là cách đo ra những con số đáng tin. Bài này không mổ một cơ chế mới; nó chưng cất kỷ luật đo của cả sê-ri, và minh họa bằng đúng một phép đo cho thấy: cùng một việc, đo sai và đo đúng lệch nhau tới mức nào.

Đo lường đúng cách

Một phép đo, hai kết quả

Tôi đo thời gian cộng một mảng 2 triệu số nguyên — một "công việc" nhỏ, rõ ràng. Đo theo hai cách.

Cách sai (một lần, chưa làm nóng): chạy đúng một lần, đọc đồng hồ. Kết quả: 0,459 ms. Gọn gàng, có vẻ dùng được.

Cách đúng (lặp 201 lần, lấy trung vị): chạy lại 201 lần, sắp xếp, nhìn phân bố:

min       = 0,453 ms
trung vị  = 0,485 ms
max       = 2,072 ms   <- gấp 4,3 lần trung vị!

Con số "một lần" của tôi (0,459 ms) tình cờ nhanh hơn trung vị 5% — tôi gặp may. Nhưng nhìn cột max: một lần chạy có thể ra 2,072 ms, gấp hơn bốn lần. Nếu lần đo duy nhất của tôi rơi trúng cái đuôi đó, tôi đã báo cáo con số sai gấp bốn mà không hề biết. Một lần đo là một cú tung xúc xắc; cái đuôi (do nhiễu hệ thống, lập lịch, cache lạnh, ảo hóa) luôn rình ở đó. Lặp tới ổn định rồi lấy trung vị mới cho con số không phụ thuộc may rủi.

Khi công cụ nói dối: số quá đẹp

Phép đo thứ hai còn nguy hiểm hơn. Tôi viết một vòng lặp cộng dồn 2 tỉ số, biên dịch với -O2 (tối ưu), và đo:

-O2 : 2 tỉ vòng lặp  ->  0,000042 ms
-O0 : 2 tỉ vòng lặp  ->  3463 ms

Bốn phần trăm nghìn của một mili giây cho 2 tỉ phép cộng? Đó là khoảng 47 nghìn tỉ phép tính mỗi giây — bất khả trên phần cứng này. Con số đẹp tới mức vô lý ấy là dấu hiệu công cụ (ở đây là trình biên dịch) đang nói dối: -O2 nhận ra tổng có công thức đóng, tính sẵn kết quả lúc biên dịch và xóa luôn vòng lặp. Tôi không đo tốc độ vòng lặp; tôi đo tốc độ in một hằng số. Bản -O0 (không tối ưu) chạy vòng lặp thật và cho 3463 ms — con số có thật.

Đây là một trong những cái bẫy tôi vấp nhiều lần suốt sê-ri, dưới nhiều hình dạng: vòng lặp đo bị -O2 gấp thành 0,000 s, đệ quy bị tối ưu đuôi thành lặp vô hạn. Bài học chung: một con số quá đẹp là một con số đáng nghi ngang với một con số quá xấu. Cách chống: dùng volatile để giữ kết quả, đối chứng -O0 với -O2, và luôn hỏi "phép đo này có khả thi về mặt vật lý không?".

Một lần tôi đo hớ: bài này là tổng của mọi lần đo hớ

Mỗi bài trong sê-ri có một mục "tôi đo hớ", và bài chốt này chính là tổng của chúng. Nhìn lại, các lần vấp lặp lại vài khuôn mẫu giống nhau:

  • Cột seconds của strace -c báo 0,73 giây cho các lời write, trong khi cả chương trình chạy tự do chỉ 26 ms — vì công cụ đo (ptrace) làm chậm thứ nó đo 270 lần. Tôi tin con số của bộ đếm hơn đồng hồ.
  • calloc 500MB mà RSS vẫn ~0 — vì nhân dùng trang-zero chung, không thật sự chạm. Tôi hiểu sai cơ chế nên dự đoán sai.
  • /proc/self/stat báo CPU của tôi = 0 sau khi đốt CPU — vì self là tiến trình đang đọc, không phải chương trình tôi nghĩ. Tôi đo nhầm đối tượng.
  • Thời gian trôi qua ra số âm khi đồng hồ tường bị vặn lùi — vì tôi đo bằng đồng hồ có thể giật, thay vì đồng hồ đơn điệu.

Khuôn mẫu chung: mỗi lần con số vô lý xuất hiện, thủ phạm không phải "máy hỏng" mà là một trong ba thứ — tôi đo nhầm đại lượng, có một biến ẩn tôi chưa kiểm soát, hoặc tôi tin nhầm một công cụ đang tự làm nhiễu. Con số bất khả không phải ngõ cụt; nó là cái biển chỉ đường: "hãy hỏi lại xem mình đang đo cái gì".

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

Hệ quả đầu tiên gói lại kỷ luật đo thành vài câu dùng được ngay. Đo THẬT trong môi trường thật, đừng suy từ lý thuyết — cả sê-ri này lộ ra bao nhiêu chỗ môi trường (ảo hóa, THP, không có PMU, /tmp là overlayfs) làm số lệch khỏi sách vở. Lặp tới ổn định, lấy trung vị, không tin một lần chạy. Đọc bằng chứngerrno, /proc, getrusage, strace — chứ đừng đoán từ cảm giác. Không nuốt lỗi bằng /dev/null: một chương trình "trông vẫn chạy" có thể đang lặng lẽ thất bại ở mỗi lời gọi.

Hệ quả thứ hai là ba câu hỏi phản xạ khi một con số xuất hiện. Số này có khả thi không? — nếu quá đẹp hay quá xấu, nghi công cụ (loop bị xóa, bộ đếm phồng). Có hai số nào mâu thuẫn không? — nếu có, tôi đang đo nhầm đại lượng hoặc có biến ẩn (khác lõi, khác cache, khác điều kiện). Hai bên so sánh có làm cùng một việc trong cùng điều kiện không? — một benchmark chỉ có nghĩa khi hai phía thật sự đối xứng. Và bao trùm tất cả: khi công cụ đo tự làm nhiễu thứ nó đo (như strace), con số nó cho là con số của "thứ bị soi", không phải thứ thật.

Hệ quả thứ ba, và là câu chốt của cả sê-ri. Con số mang theo: một phép đo là một GIẢ THUYẾT phải kiểm chứng, không phải một chân lý để tin ngay — đo một lần ra 0,459ms có thể lệch 4 lần vì đuôi nhiễu, và 2 tỉ vòng lặp ra 0,000042ms là trình biên dịch nói dối. "Nhanh" là một từ vô nghĩa cho tới khi bạn hỏi thêm "nhanh so với cái gì, đo thế nào, và đã đúng/bền/tốn gì chưa". Bốn mươi lăm bài đo đủ thứ cơ chế, nhưng nếu chỉ giữ lại một điều, hãy giữ điều này: đừng tin con số — hãy tra hỏi nó, vì cái máy, cái trình biên dịch, và cái công cụ đo đều sẵn sàng cho bạn một câu trả lời gọn gàng và sai.

Thử ba mươi giây

Lần tới bạn đo hiệu năng bất cứ thứ gì, làm đúng ba việc nhỏ. Một: chạy phép đo ít nhất năm lần và nhìn khoảng dao động — nếu lần chậm nhất gấp đôi lần nhanh nhất, một-lần-đo của bạn là vô nghĩa, hãy lấy trung vị. Hai: hỏi "con số này có khả thi không" — một tỉ phép tính trong một mili giây, hay một vòng lặp nặng tốn 0,000 giây, là trình biên dịch đã xóa việc của bạn. Ba: khi hai phép đo cãi nhau, đừng vội chọn cái bạn thích — một trong hai đang đo nhầm thứ, và tìm ra thứ đó thường dạy bạn nhiều hơn cả con số ban đầu. Đó là toàn bộ những gì 45 bài này gói lại.