Suốt sê-ri này ta đã dùng go test -bench để đo hiệu năng, và nó tuyệt vời. Nhưng có một cạm bẫy nếu chỉ chạy một lần: con số bạn nhận là một mẫu duy nhất từ một hệ thống đầy nhiễu — tải nền, chu kỳ GC, tần số CPU thay đổi (turbo boost, throttling nhiệt), tranh chấp cache. Cùng một benchmark chạy hai lần cho hai con số khác nhau. Nếu bạn so "bản cũ 100ns" với "bản mới 95ns" và kết luận "nhanh hơn 5%", rất có thể 5% đó chỉ là nhiễu — chạy lại có khi bản cũ lại nhanh hơn.
benchstat (công cụ chính thức từ golang.org/x/perf) giải bằng thống kê: chạy benchmark nhiều lần, tính trung bình và độ lệch, và khi so hai phiên bản, tính p-value để cho biết khác biệt là thật hay chỉ nhiễu. Bài này đo thật quy trình đúng để benchmark có ý nghĩa.
Vì sao một lần đo không đủ
// Chạy go test -bench MỘT lần cho MỘT con số - nhưng máy có nhiễu:
// tải nền, GC, tần số CPU đổi, cache... Con số đó dao động mỗi lần.
// So hai phiên bản bằng 1 con số mỗi bên -> kết luận có thể do nhiễu.
// benchstat: chạy NHIỀU lần, tính trung bình + độ lệch + p-value.
Bước 1: chạy -count nhiều lần, lưu ra tệp
$ go test -bench=. -benchmem -run=^$ -count=10 > bench.txt
Cờ -count=10 chạy mỗi benchmark 10 lần, cho 10 mẫu mỗi hàm. Lưu ra tệp để benchstat đọc và phân tích.
Bước 2: benchstat tính trung bình ± độ lệch
$ benchstat bench.txt
GhepCong-10 19.81µ ± 1% // trung bình 10 lần, dao động ±1%
GhepBuilder-10 1.081µ ± 3% // ±% cho biết đo có ỔN ĐỊNH không
Thay vì một con số, benchstat cho trung bình ± phần trăm dao động. Con số ±% cực kỳ quan trọng: ±1% nghĩa là phép đo rất ổn định, kết luận đáng tin; ±20% nghĩa là đo nhiễu, cần chạy lại hoặc tắt tải nền trước khi tin bất kỳ con số nào.
Bước 3: so hai phiên bản với p-value
Đây là giá trị lớn nhất của benchstat — so before/after một cách có ý nghĩa thống kê:
$ go test -bench=Ghep -count=10 > old.txt # bản cũ
# ... sửa code ...
$ go test -bench=Ghep -count=10 > new.txt # bản mới
$ benchstat old.txt new.txt

Hình 1: benchstat trong Go — một lần đo là một mẫu nhiễu nên phải chạy -count=N lấy nhiều mẫu; benchstat tính trung bình ± độ lệch, và so hai bản bằng p-value để biết khác biệt là thật hay nhiễu.
Đo thật: trung bình, độ lệch, và p-value
So hai cách nối 200 chuỗi — += (tạo chuỗi mới mỗi lần) và strings.Builder (buffer lớn dần).
Một lần đo cho hai con số trần: GhepCong 18696 ns/op, GhepBuilder 1001 ns/op. Nhưng ta không biết chúng dao động bao nhiêu qua các lần chạy.
benchstat với -count=10 cho trung bình kèm độ lệch: GhepCong 19.81µ ± 1%, GhepBuilder 1.081µ ± 3%. Cả hai ±% nhỏ nên phép đo ổn định, đáng tin.
So bản cũ vs mới (cùng benchmark Ghep, đổi cài đặt bên trong):
│ old.txt │ new.txt │
│ sec/op │ sec/op vs base │
Ghep-10 19.964µ ± 3% 1.030µ ± 2% -94.84% (p=0.000 n=10)
│ B/op │
Ghep-10 170.039Ki ± 0% 5.242Ki ± 0% -96.92% (p=0.000)
Bản mới nhanh hơn 94.84% và tốn ít bộ nhớ hơn 96.92%, với p=0.000 — nghĩa là khác biệt chắc chắn thật, không phải nhiễu. Đây là kết luận có căn cứ thống kê, không phải "tôi thấy nó nhanh hơn".

Hình 2: Đo thật — một lần đo cho con số trần; benchstat với -count=10 cho trung bình ± độ lệch (19.81µ ±1%); so bản cũ/mới cho -94.84% (p=0.000 n=10) (khác biệt thật) trong khi hàm không đổi báo ~ (p=1.000).
benchstat phân biệt thật vs nhiễu bằng p-value
Điểm tinh tế nhất: benchstat cho bạn biết khi nào không có khác biệt. Với hàm không sửa:
GhepCong-10 170.0Ki 170.0Ki ~ (p=1.000 n=10) // KHÔNG đổi
benchstat báo ~ (p=1.000) nghĩa là "không có khác biệt đáng tin" — đúng, vì hàm này không đổi. Quy tắc đọc: p < 0.05 → khác biệt đáng tin (thật); p lớn hoặc ~ → chỉ là nhiễu, đừng kết luận có cải thiện/thoái lui. Đây chính là cái mà so một-con-số không bao giờ cho bạn: sự phân biệt giữa tín hiệu và nhiễu.
Đánh đổi cần cân nhắc
Cần chạy nhiều lần — tốn thời gian. -count=10 mất gấp 10 lần một lần chạy; benchmark chậm hoặc bộ nhiều benchmark có thể mất phút đến hàng chục phút. Với vòng lặp phát triển nhanh, có thể dùng -count nhỏ hơn (5) để định hướng, và -count lớn (10-20) khi cần kết luận cuối cùng. Đừng bỏ hẳn -count chỉ vì vội — một con số vội vàng dễ dẫn tới kết luận sai còn tốn thời gian hơn.
Máy phải yên tĩnh để đo đáng tin. ±% cao thường không phải lỗi benchstat mà là máy nhiễu: trình duyệt, IDE, cập nhật nền, laptop chạy pin (throttle). Trước khi tin số, đóng ứng dụng nặng, cắm điện, và nếu nghiêm túc thì tắt turbo boost / cố định tần số CPU. benchstat trung thực báo ±% cao để bạn biết phép đo không đáng tin — đừng lờ nó đi rồi kết luận từ dữ liệu nhiễu.
p-value không phải phép màu, và khác biệt "đáng tin thống kê" chưa chắc "đáng quan tâm". Với đủ mẫu, benchstat có thể báo một khác biệt 0.5% là p<0.05 (thật về mặt thống kê) — nhưng 0.5% thường chẳng đáng để đánh đổi độ phức tạp code. Luôn nhìn cả delta % (độ lớn) lẫn p-value (độ tin): cải tiến phải vừa thật vừa đủ lớn để đáng làm.
Ba ý mang về
- Một lần đo benchmark là một mẫu nhiễu — máy dao động vì tải nền, GC, tần số CPU; so hai phiên bản bằng một con số mỗi bên dễ kết luận sai từ nhiễu, nên phải chạy
-count=Nlấy nhiều mẫu. - benchstat cho trung bình ± độ lệch và p-value: đo thật,
-count=10cho19.81µ ± 1%(ổn định), và so bản cũ/mới cho-94.84% (p=0.000)— cải tiến chắc chắn thật, trong khi hàm không đổi báo~ (p=1.000); đọc±%để biết đo có tin được không, đọc p-value để phân biệt thật vs nhiễu. - Nêu rõ đánh đổi: chạy nhiều lần tốn thời gian (cân
-counttheo giai đoạn), máy phải yên tĩnh để±%thấp (đừng tin dữ liệu nhiễu), và khác biệt đáng tin thống kê (p<0.05) chưa chắc đủ lớn để đáng làm — luôn nhìn cả độ lớn lẫn độ tin.
Phần sau ta đào sâu chính những cạm bẫy khiến benchmark cho số sai: cạm bẫy microbenchmark trong Go — trình biên dịch loại bỏ code chết, hằng số hóa, và cách dùng đúng để số đo phản ánh thực tế.