Bốn mươi lăm phần, hàng trăm phép đo, một thông điệp duy nhất: đo, đừng đoán. Sê-ri này bắt đầu từ một niềm tin đơn giản mà sai — rằng hiệu năng có thể suy ra từ thông số phần cứng hay độ phức tạp Big-O. Từng phần một, phép đo phá tan niềm tin đó: cùng một thao tác, cùng một số phép tính, có thể nhanh chậm khác nhau chục đến nghìn lần tùy những chi tiết mà không cuốn sách thuật toán nào nhắc tới. Phần cuối này không đo cái gì mới — nó nhìn lại toàn cảnh, đúc kết những gì con số đã dạy, và mời bạn tiếp tục tự đo trên máy của mình.
Hằng số ẩn chi phối, không phải Big-O
Bài học lớn nhất: hằng số ẩn của O(n) — thứ mà ký hiệu Big-O cố tình bỏ qua — mới là thứ chi phối thời gian chạy thật, và nó biến thiên khủng khiếp. Một vài con số tiêu biểu đã đo trên host ARM này:
- Bộ nhớ phân tầng: cùng một lệnh "đọc một ô nhớ" tốn 0,9 ns nếu dữ liệu ở L1, tới 103 ns nếu ở RAM — chênh ~113 lần. Đường cong theo kích thước dữ liệu là một bậc thang, không phải một con số.
- Cách duyệt quan trọng ngang thuật toán: duyệt ma trận theo cột chậm 17,9 lần theo hàng; nhân ma trận đổi thứ tự vòng lặp nhanh 3,1 lần — cùng số phép tính.
- Dự đoán quyết định tốc độ: một nhánh đoán được gần như miễn phí, đoán sai tốn chục chu kỳ; một gọi gián tiếp đích ổn định ~= gọi thường, đích ngẫu nhiên chậm 6,2 lần.
- Chi phí ẩn lớn: ném một ngoại lệ tốn ~800 ns — gấp 1184 lần một vòng lặp thường; memcpy nhanh 15 lần vòng chép từng byte; cắt ngang ranh giới trang phạt 29 lần.
Không con số nào trong số này đọc được từ thông số CPU hay đếm được bằng Big-O. Chúng chỉ hiện ra khi đo.
Mỗi máy một khác — đo trên máy đích
Bài học lớn thứ hai: không có con số vũ trụ. Máy tôi đo suốt sê-ri là một lõi ARM Apple Silicon (AArch64) chạy trong máy ảo Linux — và nó khác x86 ở những chỗ khiến nhiều "quy tắc tối ưu" quen thuộc trở nên thừa hoặc sai:
- Dòng cache 128 byte, không phải 64 byte như x86 — đo ngưỡng bão hòa mới biết.
- Không có SMT/Hyperthreading (Thread/core = 1) và không có NUMA (0 node, bộ nhớ phẳng) — mọi tối ưu ghim-node hay lo luồng-anh-em ở đây là thừa.
- Lệch căn trong trang gần như miễn phí (kể cả cắt dòng cache), chỉ cắt ngang trang mới phạt — ngược với nỗi ám ảnh "line-split" của x86.
memory_order_relaxedgần bằngseq_csttrên ARM ở nhiều ca, và false sharing chỉ ~2,6 lần chứ không dữ dội như x86.
Chép một quy tắc tối ưu từ kiến trúc khác mà không đo có thể khiến bạn tối ưu cho một cỗ máy không tồn tại. Con số đúng chỉ đến từ máy đích của bạn.
Một lần tôi đo hớ: "đọc thông số là biết" và "đo một máy là biết mọi máy"
Xuyên suốt sê-ri, mỗi phần mở đầu bằng một đo hớ — một trực giác hợp lý bị phép đo bác bỏ. Ở bài kết này, hai đo hớ lớn nhất gói trọn tinh thần cả loạt bài.
Thứ nhất: "đọc thông số phần cứng, biết độ phức tạp Big-O, là đủ để biết code nhanh hay chậm". Sai — và sai hàng chục tới hàng nghìn lần. Hằng số ẩn (dữ liệu ở tầng cache nào, nhánh đoán được không, truy cập tuần tự hay ngẫu nhiên, có cắt ranh giới trang không) chi phối thời gian thật; hai đoạn code cùng O(n) có thể chênh nhau trăm lần. Không thể suy hiệu năng từ giấy — phải đo.
Thứ hai — cái bẫy tinh vi hơn: "được rồi, tôi đã đo trên máy tôi, vậy là biết". Nhưng đo trên một máy không cho biết mọi máy. ARM khác x86 (dòng 128 byte, không SMT/NUMA, atomic rẻ hơn, line-split miễn phí); một laptop khác một máy chủ; một máy ảo khác máy thật (như phần này thấy: VM giấu PMU, giấu NUMA). Con số tôi đo đúng cho máy này, nhưng bạn phải đo lại trên máy của bạn — vì đó mới là nơi code bạn chạy.
Bài học đo lường: ĐO, ĐỪNG ĐOÁN. Hằng số ẩn (cache, nhánh, tuần tự/ngẫu nhiên, căn lề, đồng bộ) chi phối thời gian thật hơn Big-O — cùng thao tác chênh chục-nghìn lần (đo: cache 113x, cột/hàng 17,9x, throw 1184x, page-split 29x, memcpy 15x). Mỗi máy MỘT khác (host ARM: dòng 128B, không SMT/NUMA, atomic rẻ, line-split free — quy tắc x86 thành thừa/sai). Phương pháp: container thật + SINK+warmup+min+đối chứng + báo trung thực khi không quan sát được. 'Đọc thông số/Big-O là biết hiệu năng' SAI (phải đo); 'đo một máy là biết mọi máy' SAI (phải đo máy đích). Nếu tin "đọc thông số là biết" tôi tối ưu mù; nếu tin "đo một máy là đủ" tôi mang số của máy khác áp lên máy mình.
Vì sao điều này quan trọng khi lập trình
Hệ quả đầu tiên: khi hiệu năng quan trọng, hãy viết một phép đo trước khi tin bất kỳ trực giác nào. Đừng tranh luận "cái này chắc nhanh hơn" — dựng một benchmark nhỏ (đúng cách: SINK, warmup, min, lặp-chia-N), chạy nó, đọc con số. Ba mươi giây đo thắng ba giờ suy đoán. Và khi con số khó tin (quá nhanh, quá chậm), nghi ngờ benchmark trước khi nghi ngờ phần cứng.
Hệ quả thứ hai: học vài hiện tượng vi kiến trúc cốt lõi — cache phân tầng, dự đoán nhánh, tuần tự vs ngẫu nhiên, chia sẻ dữ liệu giữa luồng — vì chúng giải thích phần lớn khác biệt hiệu năng bạn gặp. Không cần thuộc lòng mọi chi tiết; chỉ cần biết nghi ngờ đúng chỗ: code chậm bất thường mà số phép không đổi thì nghĩ tới cache và dự đoán trước tiên, rồi đo để xác nhận.
Hệ quả thứ ba, và là tinh thần của cả sê-ri: con số thắng trực giác, và con số phải đến từ máy đích của bạn. Mọi bài trong loạt này chỉ làm một việc — thay một câu "chắc là..." bằng một con số đo được, và gần như lần nào con số cũng bất ngờ. Đó là món quà của việc đo: nó liên tục cho bạn thấy thế giới không như bạn tưởng. Hãy giữ thói quen đó. Đừng tin trực giác hiệu năng của mình — kể cả sau khi đọc hết 45 phần này. Viết phép đo, chạy trên máy bạn, đọc con số. Đo, đừng đoán.
Thử ba mươi giây
Chọn một niềm tin về hiệu năng mà bạn đang mang — bất kỳ điều gì bạn tin là "nhanh hơn" hay "không đáng kể": duyệt mảng theo hàng hay cột, dùng std::vector hay std::list, một hàm ảo có đáng ngại không, memcpy hay vòng lặp tay, hằng số của thuật toán O(n log n) bạn vừa viết. Rồi đừng tin nó — viết mười lăm dòng benchmark (nhớ: ghi kết quả vào biến volatile, dùng dữ liệu khởi tạo lúc chạy, chạy nóng vài lần, lặp nhiều lần chia số phần tử, lấy giá trị nhỏ nhất) và đo nó trên chính máy bạn. Rất có thể con số sẽ làm bạn ngạc nhiên — nhanh hơn, chậm hơn, hoặc chênh nhiều lần so với bạn nghĩ. Đó là khoảnh khắc quan trọng nhất của cả sê-ri này: không phải một con số cụ thể tôi đã đo, mà là thói quen bạn vừa tập — thay một phỏng đoán bằng một phép đo. Làm nó một lần, bạn sẽ thấy nghiện; làm nó mãi, bạn sẽ viết code nhanh dựa trên sự thật thay vì niềm tin. Đo, đừng đoán — trên máy của bạn, hôm nay.