Gần như cả sê-ri này tôi chỉ đo bằng một thứ duy nhất: cái đồng hồ. Nhưng dân tối ưu chuyên nghiệp còn một món đồ mạnh hơn nhiều — bộ đếm phần cứng, hay PMU. Ngay trong CPU có sẵn những cái đếm nho nhỏ, ghi lại số lệnh đã chạy, số chu kỳ, số lần trượt cache, số lần đoán sai nhánh. Công cụ như perf stat đọc thẳng những con số đó ra cho bạn, khỏi phải suy từ thời gian.

Nếu ví cái đồng hồ như việc bấm giờ một chiếc xe chạy hết vòng đua, thì PMU giống như ngồi ngay trong buồng lái mà đọc đồng hồ vòng tua, nhiệt độ, mức xăng theo từng giây. Con số quan trọng nhất nó cho là IPC — số lệnh mỗi chu kỳ. IPC cao (3–4 trên lõi hiện đại) nghĩa là pipeline đầy, mọi thứ trơn tru; IPC thấp dưới 1 nghĩa là CPU đang khựng ở đâu đó, chờ bộ nhớ hoặc nuốt một cú đoán sai nhánh.

Nghe hấp dẫn. Nên tôi thử đọc mấy con số đó trên chính máy mình. Và đây là chỗ câu chuyện rẽ ngang.

Máy này không cho đọc counter

Có hai thứ cần để đọc được PMU: phần cứng PMU phải được kernel phơi ra, và bạn phải có quyền mở nó. Trong một container chạy trên một máy ảo, cả hai thường đều thiếu — máy ảo hay không ảo hóa PMU, còn seccomp với perf_event_paranoid thì chặn syscall.

Tôi kiểm chính cái container gcc:13 trên máy ARM đang dùng. perf thậm chí còn không được cài. Tôi gọi thẳng perf_event_open để xin đếm instructions, cpu-cycles, branch-misses — cả ba đều trả về đúng một câu: Operation not permitted. Xin một bộ đếm phần mềm loại nhẹ nhất (task-clock) cũng bị từ chối nốt. Ngó vào /sys/bus/event_source/devices/ thì chỉ thấy software, tracepoint, kprobe, tuyệt nhiên không có thiết bị PMU phần cứng nào. Máy ảo này giấu biệt cái PMU đi.

Nghĩa là tôi không đọc được IPC hay branch-miss thật ở đây. Và tôi nói thẳng vậy, chứ không bịa vài con số cho ra vẻ chuyên nghiệp.

Không có counter vẫn tóm được branch-miss

Chuyện hay là dù không có cái đồng hồ vòng tua, tôi vẫn nhìn thấy branch-miss — bằng đúng cái mẹo cả sê-ri này xài. Lấy một vòng lặp if (a[i] >= 128) s += a[i] (nhớ ép giữ nhánh thật bằng -fno-if-conversion), rồi chạy nó trên cùng một bộ dữ liệu nhưng theo hai thứ tự:

  • Dữ liệu đã sắp xếp (nhánh dễ đoán): 0,001 ns/phần tử
  • Dữ liệu ngẫu nhiên (nhánh khó đoán, sai chừng một nửa): 0,008 ns/phần tử — chậm 6,94 lần

Cái hiệu ứng branch-miss lồ lộ ngay đó, đọc được bằng đồng hồ mà không cần bất kỳ bộ đếm branch-miss nào. Đây chính là "perf của nhà nghèo": một thí nghiệm A/B khéo, đổi đúng một biến (thứ tự dữ liệu), giữ nguyên mọi thứ còn lại, rồi đọc chênh lệch thời gian. Suốt 42 bài trước tôi đã dựng lại cache-miss qua bậc thang độ trễ, dựng lại stall qua thời gian khựng, tất cả chỉ bằng cách này. Chìa khóa không nằm ở món đồ đắt tiền, mà ở cách bố trí thí nghiệm.

Vậy khi nào mới thật cần counter?

Đừng vội kết luận "thế thì cần gì perf". Đồng hồ có giới hạn thật của nó. Nó cho biết code chậm, và với một thí nghiệm A/B gọn gàng thì gợi ý được thủ phạm. Nhưng khi nhiều nguyên nhân chồng lên nhau cùng lúc — một vòng lặp vừa trượt cache, vừa đoán sai nhánh, vừa kẹt chuỗi phụ thuộc — thì đồng hồ chỉ thấy một cục "chậm" tổng, không tách ra được cái nào góp bao nhiêu. Lúc ấy bộ đếm phần cứng mới ra tay: nó phang thẳng "IPC 0,8, branch-miss 12%, L1-miss 30%", chỉ đích danh kẻ gây rối, khỏi phải dựng cả loạt thí nghiệm để cô lập từng cái.

May là ngoài đời thật bạn thường có counter. Trên một máy Linux thật (không phải VM khóa PMU), perf stat ./chương-trình cho ngay IPC và các tỉ lệ miss, perf record với perf report chỉ ra hàm nào ngốn nhiều nhất. Nếu quyền bị chặn, ngó cat /proc/sys/kernel/perf_event_paranoid — số càng thấp (tới -1) càng mở. Và không riêng C: bên Java, async-profiler và JFR đọc đúng những hardware counter này để dựng flame graph; Go thì có pprof. Cùng một ý tưởng, đọc thẳng sự thật từ phần cứng thay vì đoán.

Nếu muốn tự kiểm: gõ thử perf stat ls trên máy bạn. Chạy được thì bạn thấy ngay IPC với đủ loại miss. Còn nếu nó nhè ra "not supported" hay "Operation not permitted" như cái VM của tôi, đừng nản — quay lại với đồng hồ. Viết vòng if (a[i] >= 128) sum += a[i], chạy trên mảng đã sắp xếp rồi mảng ngẫu nhiên, so thời gian; bản ngẫu nhiên chậm mấy lần là bạn vừa đo được branch-miss mà chẳng cần counter nào. Cái đáng mang theo không phải một công cụ cụ thể, mà là cái tư duy: đổi một biến, giữ phần còn lại, đọc chênh lệch. Nó chạy ở mọi nơi, kể cả nơi perf bó tay.