Suốt 42 phần, sê-ri này đo hiệu năng vi kiến trúc bằng một công cụ duy nhất: đồng hồ. Nhưng có một công cụ mạnh hơn mà dân tối ưu chuyên nghiệp hay dùng — bộ đếm phần cứng (PMU, Performance Monitoring Unit). CPU có sẵn các bộ đếm sự kiện: số lệnh thực thi, số chu kỳ, số cache-miss, số branch-miss, và nhiều nữa. Công cụ như perf stat đọc chúng, cho bạn con số trực tiếp — IPC (instructions per cycle, lệnh mỗi chu kỳ), tỉ lệ cache-miss, tỉ lệ branch-miss — thay vì phải suy ra từ thời gian. Câu hỏi phần này: đọc chúng thế nào, và khi nào cần? Tôi thử trong container gcc:13 trên host ARM — và như hai phần trước (SMT, NUMA), phải báo cáo trung thực máy này có đọc được counter không.
PMU đếm sự kiện thật — nếu bạn đọc được nó
IPC là thước đo trung tâm: nếu CPU chạy được nhiều lệnh mỗi chu kỳ (IPC cao, 3–4 trên lõi hiện đại), pipeline đầy và mọi thứ trơn tru; nếu IPC thấp (dưới 1), CPU đang khựng — chờ bộ nhớ, chờ chuỗi phụ thuộc, hoặc nuốt đoán sai nhánh. Bộ đếm branch-miss cho biết chính xác bao nhiêu nhánh đoán sai; bộ đếm cache-miss cho biết bao nhiêu lần chạm bộ nhớ trượt L1/L2/L3. Đây là sự thật phần cứng, không phải suy đoán.
Nhưng đọc được chúng cần hai thứ: một PMU phần cứng mà kernel phơi ra, và quyền để mở nó (perf_event_open, thường bị perf_event_paranoid giới hạn). Trong một container trên một máy ảo, cả hai thường thiếu: máy ảo hay không ảo hóa PMU, và seccomp/paranoid chặn syscall. Tôi kiểm chính máy đang chạy.
Đo: không đọc được counter, nhưng timing vẫn thấy branch-miss
Host ARM (AArch64) trong container gcc:13:
(1) Mở bộ đếm PHẦN CỨNG qua perf_event_open():
instructions -> Operation not permitted
cpu-cycles -> Operation not permitted
branch-misses -> Operation not permitted
(software task-clock cũng -> not permitted)
/sys/bus/event_source/devices: chỉ có software/tracepoint, KHÔNG có PMU phần cứng
=> không đọc được bộ đếm nào ở đây.
(2) Suy ra branch-miss chỉ bằng TIMING (cùng kernel, cùng dữ liệu, khác thứ tự):
dữ liệu SẮP XẾP (nhánh đoán được) | 0,001 ns/phần tử
dữ liệu NGẪU NHIÊN (nhánh khó đoán)| 0,008 ns/phần tử -> 6,94x
Đọc ra hai điều. Một (không có PMU ở đây): perf thậm chí không được cài trong container; và khi tôi thử gọi thẳng perf_event_open cho instructions, cpu-cycles, branch-misses, cả ba đều trả "Operation not permitted" — kể cả một bộ đếm phần mềm (task-clock). Kiểm /sys/bus/event_source/devices/ chỉ thấy software, tracepoint, kprobe… mà không có thiết bị PMU phần cứng (cpu/armv8_pmuv3). Máy ảo này không phơi PMU ra. Nên tôi không thể đọc IPC hay branch-miss thật ở đây — và tôi nói thẳng vậy, thay vì bịa vài con số counter cho ra vẻ.
Hai (timing vẫn lộ branch-miss): dù không có counter, tôi vẫn thấy được branch-miss — bằng đúng phương pháp cả sê-ri dùng. Cùng một kernel if (a[i] >= 128) s += a[i] (giữ nhánh thật bằng -fno-if-conversion), chạy trên cùng dữ liệu nhưng sắp xếp (nhánh đoán được: một chuỗi false rồi một chuỗi true) so với ngẫu nhiên (nhánh khó đoán, đoán sai ~50%): bản sắp xếp 0,001 ns/phần tử, bản ngẫu nhiên 0,008 — chậm 6,94 lần. Cái hiệu ứng branch-miss hiện ra rõ ràng qua thời gian, không cần đọc bộ đếm branch-miss. Đây chính là "perf của người nghèo": một thí nghiệm A/B khéo léo làm lộ nguyên nhân vi kiến trúc bằng đồng hồ.
Một lần tôi đo hớ: "phải có perf" và "timing là đủ"
Tôi vào đo với niềm tin phụ thuộc công cụ: "muốn đo hiệu năng vi kiến trúc — cache-miss, branch-miss, IPC — thì phải có perf và bộ đếm phần cứng, không có thì bó tay". Sai: cả 42 phần trước của sê-ri này đã đo những thứ đó chỉ bằng timing — branch-miss (phần 1–2) qua thời gian đoán sai, cache-miss (phần 5, 41) qua bậc thang độ trễ, IPC thấp/stall (phần 3, 11) qua thời gian khựng. Và ngay ở đây, không có perf, tôi vẫn thấy branch-miss 6,94 lần. Chìa khóa không phải công cụ đắt tiền, mà là thí nghiệm đối chứng: đổi một biến (thứ tự dữ liệu) giữ mọi thứ khác cố định, rồi đọc khác biệt thời gian.
Nhưng đo cũng chỉnh niềm tin ngược: "vậy timing là đủ, không bao giờ cần counter phần cứng". Sai — timing có giới hạn thật. Nó cho biết code chậm, và với một thí nghiệm A/B khéo, gợi ý nguyên nhân; nhưng khi nhiều nguyên nhân chồng nhau (một vòng lặp vừa cache-miss, vừa branch-miss, vừa stall phụ thuộc cùng lúc), timing chỉ thấy tổng "chậm" mà không tách bạch được cái nào đóng góp bao nhiêu. Lúc đó bộ đếm phần cứng cho con số tuyệt đối và riêng rẽ — "IPC 0,8, branch-miss 12%, L1-miss 30%" — chỉ thẳng thủ phạm, thay vì phải thiết kế hàng loạt thí nghiệm để cô lập. Trên máy production thật (không phải VM khóa PMU), perf stat và perf record là công cụ vô giá.
Bài học đo lường: PMU/perf đọc bộ đếm phần cứng (IPC, cache-miss, branch-miss) TRỰC TIẾP — nhưng cần PMU được phơi + quyền. Máy đo (VM ARM) KHÔNG có: perf_event_open trả 'Operation not permitted', /sys không có PMU phần cứng. Dù vậy TIMING + thí nghiệm đối chứng vẫn lộ branch-miss (đo cùng kernel: data sắp xếp 0,001 vs ngẫu nhiên 0,008 ns = 6,94x). 'Phải có perf mới đo được vi kiến trúc' SAI (cả sê-ri suy ra bằng timing); 'timing là đủ, không cần counter' SAI (nhiều nguyên nhân chồng nhau thì chỉ counter tách bạch được). Nếu tin "phải có perf" tôi bỏ cuộc khi không có nó; nếu tin "timing là đủ" tôi bó tay khi nhiều nguyên nhân trộn lẫn.
Vì sao điều này quan trọng khi lập trình
Hệ quả đầu tiên: học cách đo bằng thí nghiệm đối chứng — nó chạy ở mọi nơi, kể cả khi không có perf. Muốn biết một vòng lặp chậm vì branch-miss? Chạy nó trên dữ liệu đoán được vs ngẫu nhiên và so thời gian. Nghi cache-miss? Quét working set từ nhỏ tới lớn xem có bậc thang không. Đây là kỹ năng nền, không phụ thuộc công cụ — và là cách cả sê-ri này đo mọi hiện tượng.
Hệ quả thứ hai: khi có thể, dùng perf để lấy con số tuyệt đối và tách bạch nguyên nhân. Trên máy phát triển hay production thật (không phải VM khóa PMU), perf stat ./chương-trình cho bạn IPC, cache-miss, branch-miss ngay; perf record/perf report chỉ ra hàm nào tốn nhiều. Khi timing nói "chậm" mà bạn không rõ vì sao, counter là câu trả lời trực tiếp. Kiểm quyền: cat /proc/sys/kernel/perf_event_paranoid (giá trị thấp/-1 cho phép nhiều hơn).
Hệ quả thứ ba là tinh thần đo lường: counter phần cứng và timing bổ sung cho nhau — biết dùng cả hai, và biết khi nào máy không cho bạn counter. Con số mang theo: perf/PMU đọc IPC/miss trực tiếp nhưng cần được phơi + quyền (VM thường khóa — đo được ở đây: không mở nổi); timing + thí nghiệm đối chứng suy ra cùng hiện tượng ở mọi nơi (branch-miss 6,94x không cần counter); nhiều nguyên nhân chồng nhau thì cần counter để tách. Công cụ tốt nhất là công cụ bạn có — và một thí nghiệm A/B khéo với đồng hồ đi được rất xa.
Thử ba mươi giây
Trên máy của bạn, thử perf stat ./chương-trình (hoặc perf stat -e instructions,cycles,branch-misses,cache-misses ls): nếu chạy, bạn thấy ngay IPC và các tỉ lệ miss — con số phần cứng thật. Nếu nó báo "not supported" hay "Operation not permitted" (máy ảo, container, hoặc perf_event_paranoid cao), thì máy bạn không cho đọc counter — như VM tôi đo. Không sao: làm thí nghiệm đối chứng thay thế. Viết một vòng lặp if (a[i] >= 128) sum += a[i] và chạy nó hai lần — một lần trên mảng đã sắp xếp, một lần trên mảng ngẫu nhiên cùng dữ liệu — rồi so thời gian. Bản ngẫu nhiên sẽ chậm vài lần (trên máy tôi 6,94 lần), và bạn vừa đo được branch-miss mà không cần bộ đếm branch-miss nào. Rồi thử với working set nhỏ vs lớn để thấy cache-miss qua bậc thang thời gian. Ba mươi giây đó cho bạn thấy điều mà "phải có perf mới đo được" giấu đi: bộ đếm phần cứng tuyệt vời khi có, nhưng phương pháp — đổi một biến, giữ phần còn lại, đọc khác biệt thời gian — mới là thứ cho bạn thấy sự thật vi kiến trúc ở bất kỳ máy nào.