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

Ảnh chụp đoạn mã và lệnh nền tối minh hoạ benchmark chính xác với benchstat trong Go một con số đơn lẻ dễ lừa, vì sao một lần đo là một mẫu nhiễu 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 cộng độ lệch cộng p-value, bước 1 chạy count nhiều lần lưu ra tệp go test bench benchmem run count 10 lớn hơn bench.txt count 10 chạy mỗi benchmark 10 lần 10 mẫu cho mỗi hàm lưu ra tệp để benchstat đọc và phân tích thống kê, bước 2 benchstat tính trung bình cộng độ lệch benchstat bench.txt GhepCong 19.81µ cộng trừ 1 phần trăm trung bình 10 lần dao động GhepBuilder 1.081µ cộng trừ 3 phần trăm cho biết đo có ổn định không dao động phần trăm cao đo nhiễu cần chạy lại tắt tải nền dao động phần trăm thấp tin được, bước 3 so hai phiên bản cũ vs mới có ý nghĩa thống kê go test bench Ghep count 10 lớn hơn old.txt bản cũ sửa code go test bench Ghep count 10 lớn hơn new.txt bản mới benchstat old.txt new.txt hiện delta phần trăm và p-value khác biệt có thật hay chỉ nhiễu p nhỏ hơn 0.05 khác biệt đáng tin p xấp xỉ 1.000 không đổi nhiễu

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".

Ảnh chụp bảng kết quả đo thật nền tối benchstat trong Go chạy bằng go test bench count 10 cộng benchstat Go 1.23 arm64 nối 200 chuỗi, 1 một lần đo hai con số trần BenchmarkGhepCong 18696 ns mỗi op 174121 B mỗi op 199 allocs BenchmarkGhepBuilder 1001 ns mỗi op 5368 B mỗi op 10 allocs một con số mỗi bên không biết dao động bao nhiêu qua các lần, 2 benchstat với count 10 trung bình cộng độ lệch GhepCong 19.81µ cộng trừ 1 phần trăm TB 10 lần ổn định GhepBuilder 1.081µ cộng trừ 3 phần trăm vẫn tin được geomean 4.628µ dao động phần trăm nhỏ phép đo ổn định kết luận đáng tin, 3 so bản cũ vs mới delta phần trăm cộng p-value old.txt sec op new.txt sec op vs base Ghep 19.964µ cộng trừ 3 phần trăm 1.030µ cộng trừ 2 phần trăm âm 94.84 phần trăm p bằng 0.000 n bằng 10 bản mới Builder nhanh hơn 94.84 phần trăm p 0.000 khác biệt thật B op Ghep 170.039Ki 5.242Ki âm 96.92 phần trăm p 0.000 bộ nhớ giảm 96.92 phần trăm cũng đáng tin thống kê, benchstat phân biệt thật vs nhiễu bằng p-value GhepCong 170.0Ki 170.0Ki xấp xỉ p bằng 1.000 n bằng 10 không đổi hàm không sửa benchstat báo dấu ngã p 1.000 không khác biệt p nhỏ hơn 0.05 khác biệt đáng tin p lớn dấu ngã chỉ là nhiễu đừng kết luận, cốt lõi vấn đề 1 lần đo là 1 mẫu nhiễu so bằng 1 con số dễ sai count N chạy N lần lấy N mẫu cho mỗi benchmark benchstat trung bình cộng độ lệch phần trăm nhỏ mới tin được p-value so 2 bản p nhỏ hơn 0.05 khác biệt thật p xấp xỉ 1.000 chỉ nhiễu đo thật Builder vs Cong âm 94.84 phần trăm p 0.000 thật hàm cũ dấu ngã p 1.000 đánh đổi cần chạy nhiều lần tốn thời gian máy phải yên tĩnh

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ề

  1. 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=N lấy nhiều mẫu.
  2. benchstat cho trung bình ± độ lệch và p-value: đo thật, -count=10 cho 19.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.
  3. Nêu rõ đánh đổi: chạy nhiều lần tốn thời gian (cân -count theo 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ế.