Đồng hồ bấm giờ cho biết vận động viên chạy hết đường đua trong bao lâu. Nhưng muốn biết vì sao anh ta chậm — sải chân ngắn, thở sai nhịp, hay xuất phát chậm — bạn cần camera quay chậm phân tích từng động tác, một thiết bị hoàn toàn khác. perf là cái camera quay chậm đó cho CPU: nó không đo nhanh hay chậm mà đo vì sao. Ở bài trước ta đo thời gian — chỉ biết nhanh chậm. Tôi định dùng perf nhìn sâu hơn trong bài này, và vấp đúng một giới hạn của môi trường ảo hóa mà cả sê-ri đã gặp ở nhiều dạng: đôi khi cái camera quay chậm đó đơn giản là không có mặt.
perf đọc bộ đếm phần cứng của CPU
perf không đo bằng đồng hồ; nó đọc các bộ đếm hiệu năng (performance counters) nằm trong một khối phần cứng của CPU gọi là PMU (Performance Monitoring Unit — đơn vị giám sát hiệu năng). Trong khi CPU chạy, PMU tự động đếm các sự kiện ở mức vi kiến trúc: số lệnh hoàn thành, số chu kỳ đồng hồ, số cache-miss (lần truy cập bộ nhớ trượt cache phải xuống RAM), số branch-miss (lần đoán sai một rẽ nhánh).
Từ những con số đó ta rút ra IPC (instructions per cycle — số lệnh mỗi chu kỳ), thước đo trực tiếp code chạy hiệu quả tới đâu trên phần cứng: IPC cao nghĩa là CPU luôn có việc làm, IPC thấp nghĩa là nó thường xuyên phải chờ (chờ bộ nhớ vì cache-miss, hay đoán sai nhánh phải làm lại). Đây là mức chi tiết mà thời gian không cho thấy: hai chương trình cùng chạy 1 giây có thể có IPC khác hẳn, và cái IPC thấp mới là cái cần tối ưu. Tôi định dùng perf đo cache-miss của truy cập tuần tự so với ngẫu nhiên — nhưng công cụ không chạy.
Cái camera quay chậm không có mặt: PMU vắng trong máy ảo
Đầu tiên which perf cho ra rỗng — không có bản perf cài sẵn. Không sao, tôi gọi thẳng syscall perf_event_open trong C để mở một bộ đếm phần cứng. Kết quả với mọi bộ đếm phần cứng:
perf_event_open(HW instructions) -> fd=-1 No such file or directory (ENOENT)
perf_event_open(HW cache-misses) -> fd=-1 No such file or directory (ENOENT)
perf_event_open(HW branch-misses) -> fd=-1 No such file or directory (ENOENT)
perf_event_open(SW page-faults) -> fd=3 OK (đếm được)
Một ENOENT — "No such file or directory" — cho việc mở một bộ đếm lệnh CPU nghe rất kỳ. Không có "file" nào ở đây cả, sao lại "không tìm thấy file"? Nhưng đây chính là cách nhân báo: không có event source phần cứng để mở. Nhìn vào /sys/bus/event_source/devices xác nhận: chỉ có software, tracepoint, kprobe, breakpoint — không có cpu. Máy ảo này (chạy trên Apple Silicon) không expose PMU phần cứng cho guest.
Đây có lẽ là bài học đo lường trung thực nhất của sê-ri: bộ đếm phần cứng là một năng lực của phần cứng, và không phải máy nào cũng cho bạn dùng. Máy ảo ảo hóa CPU, và PMU — một khối vật lý của con chip — thường không được chuyền qua cho guest (ảo hóa nó vừa khó vừa tốn kém). Nên trong container này tôi không thể đo IPC hay cache-miss trực tiếp; con số ENOENT là môi trường thẳng thắn nói vậy, không phải lỗi code của tôi. Nếu tôi cứ cố và bịa ra vài con số cache-miss, tôi đã vi phạm chính nguyên tắc đo thật của sê-ri.
Điểm sáng: bộ đếm phần mềm vẫn chạy — perf_event_open(SW page-faults) trả về fd hợp lệ, vì lỗi trang do nhân đếm chứ không cần PMU phần cứng. Nên một phần của perf (các sự kiện nhân đếm: lỗi trang, chuyển ngữ cảnh, di trú) vẫn dùng được ngay cả khi không có PMU.
Sự bất đối xứng này rất đáng nhớ: cùng một syscall perf_event_open, cùng một chương trình, mà bộ đếm phần cứng chết còn bộ đếm phần mềm sống. Nó phân định rạch ròi hai loại "đếm": loại nhân tự đếm được bằng mã của chính nó (mỗi lần xử lý một lỗi trang, nhân cộng một biến — không cần phần cứng nào), và loại chỉ có con chip đếm được (một lệnh máy chạy qua, một dòng cache trượt — nhân đứng ngoài, không thấy, chỉ PMU thấy). Cái thứ hai là thứ ảo hóa nuốt mất.
Không có counter thì suy hiệu ứng qua thời gian
Không đọc được counter cache-miss, tôi làm cách gián tiếp: đo thời gian của hai kiểu truy cập bộ nhớ mà cache-miss sẽ quyết định. Cùng một mảng 32MB (lớn hơn cache), cộng mọi phần tử theo hai thứ tự:
tuần tự (stride 1, thân thiện cache) : 757 triệu phần tử/giây
ngẫu nhiên (thứ tự xáo trộn) : 316 triệu phần tử/giây
-> truy cập ngẫu nhiên chậm ~2,4 lần
Truy cập tuần tự nhanh vì mỗi lần nạp một dòng cache mang theo nhiều phần tử kề nhau (và bộ nạp trước đoán được bước tiếp theo). Truy cập ngẫu nhiên chậm vì mỗi phần tử ở một chỗ khác, phần lớn là cache-miss phải xuống RAM. Đúng cái mà perf stat -e cache-misses sẽ đếm trực tiếp nếu có PMU — nhưng ở đây tôi suy ra nó qua thời gian. Con số 2,4 lần khiêm tốn hơn trên máy thật (do cache lớn của chip Apple và ảo hóa bộ nhớ), nhưng hướng thì rõ: cache-miss làm chậm, và ta thấy được qua đồng hồ khi không có bộ đếm.
Về đến code hằng ngày
Điều đầu tiên: khi cần biết vì sao code chậm, perf là công cụ đúng — trên phần cứng thật. Trên một máy chủ vật lý (hay VM có PMU passthrough), perf stat ./chương-trình cho bạn instructions, cycles, IPC, cache-misses, branch-misses trong vài giây — chỉ ra ngay code nghẽn ở đâu: IPC thấp + cache-miss cao = nghẽn bộ nhớ (sửa bằng bố cục dữ liệu, tuần tự hóa truy cập); branch-miss cao = nhánh khó đoán (sửa bằng bỏ nhánh, sắp lại dữ liệu). Đây là mức chẩn đoán mà chỉ đo thời gian không cho được.
Điều thứ hai: biết công cụ đo của mình cần gì từ môi trường. perf cần PMU phần cứng; nếu bạn chạy trong container/VM và thấy perf báo "not supported" hay bộ đếm về 0, rất có thể PMU không được expose — không phải code bạn hoàn hảo, mà là đo không tới. Kiểm /proc/sys/kernel/perf_event_paranoid (số cao chặn nhiều) và /sys/bus/event_source/devices (có cpu không). Muốn dùng perf thật, chạy trên bare-metal hoặc VM bật PMU virtualization.
Và cái bẫy "công cụ đo cần một năng lực mà môi trường không cấp" này lặp lại ở mọi tầng bạn từng làm việc. Chạy JFR (Java Flight Recorder) hay async-profiler trong một container hạn chế cũng đụng đúng bức tường PMU — profiler CPU rơi về chế độ lấy mẫu kém chính xác khi không có counter phần cứng. Trong CSDL, EXPLAIN (ANALYZE, BUFFERS) cho bạn số trang đọc từ cache so với đĩa, nhưng track_io_timing phải bật và đồng hồ hệ thống phải rẻ thì con số thời gian I/O mới đáng tin. Ngay cả một docker stats trong VM lồng nhau có thể báo số CPU/bộ nhớ lệch vì tầng ảo hóa che mất cgroup thật. Mẫu số chung: trước khi tin hay đổ lỗi cho một công cụ đo, hỏi nó cần gì và môi trường có cho không — và một "không đo được" trung thực tốt hơn một con số bịa nghe rất có thẩm quyền. Gói lại bằng số: perf đọc bộ đếm phần cứng từ PMU (instructions, cache-miss, branch-miss → IPC) để chỉ ra vì sao code chậm; nhưng PMU là năng lực phần cứng mà máy ảo thường không expose — ở đây perf_event_open cho mọi bộ đếm phần cứng trả ENOENT, chỉ bộ đếm phần mềm (nhân đếm) chạy được, và hiệu ứng cache-miss phải suy qua thời gian (ngẫu nhiên chậm 2,4 lần tuần tự).
Muốn tự thấy thì trên một máy Linux vật lý, thử perf stat ./chương-trình-của-bạn (hoặc perf stat ls) — bạn sẽ thấy một bảng đẹp: instructions, cycles, insn per cycle (IPC), cache-misses, branch-misses. Nếu thay vào đó bạn thấy <not supported> hay toàn số 0, hãy kiểm hai chỗ: cat /proc/sys/kernel/perf_event_paranoid (giá trị 2 hay 3 chặn phần lớn, đặt về 1 hoặc 0 với quyền root để mở), và ls /sys/bus/event_source/devices/ (nếu không có thư mục cpu thì máy — thường là VM — không expose PMU, và perf hardware sẽ không đo được, đúng như bài này gặp). Con số bạn thấy hay không thấy chính là môi trường đo của bạn đang nói.