Bài trước thấy inline là một tối ưu quý — nhanh hơn tới 2,7 lần. Vậy tại sao lại có một directive để chặn nó? Bởi vì chính cái tối ưu đó có thể khiến benchmark của bạn đo ra một con số vô nghĩa: gần bằng 0. Nếu bạn từng viết một benchmark cho kết quả "nhanh đến khó tin", rất có thể compiler đã xoá sạch công việc bạn định đo. Bài này mổ xẻ cạm bẫy đó, cách //go:noinline cứu nó, và các directive compiler khác.
Cạm bẫy: hàm thuần bị inline rồi xoá sạch
Xét một benchmark trông hoàn toàn hợp lý:
func binhphuong(x float64) float64 { return x * x }
func BenchmarkSai(b *testing.B) {
for n := 0; n < b.N; n++ {
binhphuong(float64(n)) // kết quả bị BỎ ĐI
}
}
Vấn đề: binhphuong là hàm thuần (kết quả chỉ phụ thuộc input, không hiệu ứng phụ) và kết quả không được dùng. Sau khi compiler inline nó vào vòng lặp, nó thấy: một phép tính không ai dùng kết quả, không hiệu ứng phụ → dead code, xoá luôn. Benchmark của bạn giờ đo một vòng lặp rỗng.

Hình 1: Cạm bẫy benchmark và hai cách sửa. //go:noinline giữ ranh giới lời gọi; biến sink giữ kết quả. Directive phải nằm ngay trên func.
Đo thật: 0,23 ns hay 0,70 ns
Cùng một phép đo, ba cách viết:

Hình 2: SaiInline 0,2261 ns (SAI, bị xoá). DungNoInline 0,7047 ns và DungSink 0,8005 ns (đúng). Bản sai nhanh gấp ~3 lần con số thật.
- SaiInline (bỏ kết quả, hàm inline): 0,2261 ns — compiler xoá phần lớn công việc.
- DungNoInline (
//go:noinline): 0,7047 ns — directive giữ lời gọi, không xoá được. - DungSink (gán vào sink): 0,8005 ns — kết quả được giữ.
Bản sai đo nhanh gấp ~3 lần con số thật. Nếu tin nó, bạn kết luận sai rằng hàm "gần như miễn phí" và có thể ra quyết định thiết kế dựa trên số ma này. Đây là lý do //go:noinline (hoặc biến sink) là công cụ cần thiết cho benchmark vi mô đúng đắn.
Xác nhận directive có tác dụng
Directive im lặng nếu gõ sai (ví dụ // go:noinline có khoảng trắng thì chỉ là comment thường, không có tác dụng). Luôn kiểm bằng -gcflags=-m:
go build -gcflags=-m:
can inline sq // hàm thường: inline được
// sqNI KHÔNG xuất hiện // //go:noinline đã chặn thành công
Không thấy dòng can inline sqNI nghĩa là directive đã có hiệu lực. Cú pháp phải chính xác: //go:noinline (không có khoảng trắng sau //), đặt ngay dòng trên func, không có dòng trống chen giữa.
Ứng dụng thực tế
Benchmark vi mô chính xác. Đây là dùng phổ biến nhất. Khi đo một hàm nhỏ, //go:noinline (hoặc gán kết quả vào biến package-level sink) đảm bảo compiler không tối ưu mất thứ bạn đang đo. Nhiều thư viện benchmark chuẩn dùng cả hai để chắc chắn.
Đọc pprof/stack trace rõ ràng. Hàm được inline biến mất khỏi stack trace — nó hoà vào hàm cha. Nếu bạn cần một hàm hiện riêng trong profile CPU để quy trách nhiệm thời gian, //go:noinline giữ nó tách bạch. Đánh đổi: bạn cố tình làm chậm nó đi một chút để dễ quan sát.
Cô lập khi gỡ lỗi tối ưu. Đôi khi một tối ưu inline gây kết quả lạ (hiếm, thường là bug compiler hoặc tương tác với unsafe). //go:noinline tạm thời giúp khoanh vùng liệu inline có phải thủ phạm.
Đánh đổi cần cân nhắc
Đừng rải //go:noinline trong code production. Nó làm chậm code thật bằng cách tắt một tối ưu quý. Directive này là công cụ cho benchmark, profiling và gỡ lỗi — không phải để "kiểm soát" compiler trong code chạy thật. Để trong production là tự bắn vào chân hiệu năng.
Biến sink thường tốt hơn cho benchmark thật. //go:noinline chặn cả tối ưu xuyên hàm, nên đôi khi đo ra con số cao hơn thực tế (vì code thật của bạn sẽ được inline). Nếu bạn muốn đo hàm như nó chạy thật (được inline), dùng biến sink để giữ kết quả thay vì chặn inline — bạn giữ được tối ưu inline mà vẫn chống dead-code.
Cẩn thận cực độ với //go:nosplit và //go:noescape. Các directive họ hàng này nguy hiểm hơn nhiều: nosplit bỏ kiểm tra chia stack (có thể tràn stack), noescape khai rằng một hàm assembly không để tham số escape (sai là hỏng bộ nhớ âm thầm). Chúng dành cho runtime/thư viện tầng rất thấp, không phải code ứng dụng. //go:noinline là cái vô hại nhất trong nhóm.
Ba ý mang về
- Inline có thể làm benchmark đo ra số 0 vô nghĩa: đo thật, benchmark một hàm thuần với kết quả bị bỏ cho 0,23 ns (compiler xoá sạch bằng dead-code elimination) so với 0,70 ns thật — lệch ~3 lần, đủ dẫn tới kết luận sai.
//go:noinlinegiữ ranh giới lời gọi để đo đúng, và biếnsinkcấp package là cách thay thế thường tốt hơn (giữ được cả tối ưu inline); xác nhận directive có tác dụng bằng-gcflags=-m— gõ sai cú pháp thì nó im lặng vô hiệu.- Directive là hợp đồng cứng, chỉ dùng cho benchmark/profiling/gỡ lỗi: đừng để
//go:noinlinetrong production (nó tắt tối ưu quý), và tránh xa//go:nosplit///go:noescapetrừ khi viết runtime tầng thấp — dùng sai gây hỏng bộ nhớ âm thầm.
Phần sau ta xem một tối ưu compiler khác chạy âm thầm trong mọi truy cập slice/mảng: Phần sau mổ xẻ bounds check elimination — Go chèn kiểm tra biên ở đâu, khi nào compiler xoá được nó, và cách viết vòng lặp để giúp nó xoá.