Ở bài trước ta đo thời gian — nhưng thời gian chỉ cho biết nhanh hay chậm, không cho biết vì sao. Để nhìn vào bên trong xem CPU thật sự làm gì — bao nhiêu lệnh, bao nhiêu lần trượt cache, đoán nhánh sai bao nhiêu — có một công cụ mạnh hơn hẳn: perf. Tôi định dùng nó đo cache-miss 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.
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.
Một lần tôi đo hớ: bộ đếm phần cứng không có 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 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ì sao điều này quan trọng khi lập trình
Hệ quả đầ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.
Hệ quả 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.
Hệ quả thứ ba là bài học đo lường bao trùm: mỗi công cụ đo có điều kiện của nó, và một "không đo được" trung thực tốt hơn một con số bịa. Con số mang theo: perf đọc bộ đếm phần cứng từ PMU của CPU (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ự). 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.
Thử ba mươi giây
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.