Inlining là phép tối ưu quan trọng nhất mà compiler làm âm thầm: nó "dán" thân một hàm nhỏ thẳng vào nơi gọi, xoá đi lệnh gọi. Nhưng lợi ích thật lớn hơn nhiều so với việc tiết kiệm một lệnh CALL — và Go quyết định inline hay không bằng một cost model rất cụ thể mà bạn xem được. Bài này mổ xẻ mô hình đó bằng số liệu thật: cost của từng hàm, cái gì chặn inline, và đo tác động lên hiệu năng.
Cost model: mỗi hàm một điểm chi phí
Compiler duyệt thân mỗi hàm và cộng dồn một "chi phí" (cost): mỗi toán tử/lệnh cộng một ít, một lời gọi hàm khác cộng nhiều. Nếu tổng dưới ngân sách 80, hàm đủ nhỏ để inline. Xem cost cụ thể bằng go build -gcflags=-m=2:
can inline cong with cost 4 // return a + b
can inline max3 with cost 21 // hai câu if
can inline tong with cost 16 // có for range (Go 1.23 tính vòng lặp rẻ hơn)

Hình 1: Cost model và các cấu trúc chặn inline. cong cost 4, max3 cost 21, tong cost 16 — đều dưới ngân sách 80; defer chặn cứng, goiPhu vượt budget.
Cái gì chặn inline
Có hai loại chặn. Chặn mềm — vượt ngân sách:
cannot inline goiPhu: function too complex: cost 120 exceeds budget 80
goiPhu gọi một hàm noinline hai lần; mỗi lời gọi tới hàm không-inline được cộng chi phí rất lớn, đẩy tổng lên 120 > 80. Chặn cứng — vài cấu trúc khiến compiler từ chối bất kể cost:
cannot inline coDefer: unhandled op DEFER
Ngoài defer, các thứ chặn cứng gồm recover, goto, closure phức tạp, và một số cấu trúc khác mà bộ inline chưa xử lý. Đây là lý do đặt defer trong một hàm getter nhỏ ở đường nóng có thể âm thầm mất inline.
Vì sao inline nhanh hơn — không chỉ bỏ lệnh CALL
Đây là hiểu lầm phổ biến: "inline chỉ tiết kiệm lệnh gọi". Thực ra lợi ích lớn hơn nhiều. Khi hàm được dán vào, compiler tối ưu tiếp xuyên qua ranh giới cũ: gập hằng, loại mã chết, giữ giá trị trong thanh ghi thay vì đẩy qua stack. Ranh giới lời gọi vốn là bức tường chặn mọi tối ưu đó.
Đo thật với vòng nóng s += binhphuong(n) (binhphuong(x) = x*x), so hàm inline với cùng hàm bị //go:noinline:

Hình 2: Cost từng hàm (ngân sách 80) và tác động thật. Inline ~0,257 ns so với noinline ~0,709 ns — nhanh hơn ~2,7 lần qua 3 lần đo ổn định.
- Inline: 0,2570 / 0,2580 / 0,2565 ns/op.
- NoInline: 0,7094 / 0,7139 / 0,7040 ns/op.
Nhanh hơn ~2,7 lần. Với một hàm chỉ làm x*x, chi phí lệnh CALL thuần không thể giải thích chênh lệch này — phần lớn đến từ việc compiler, sau khi inline, giữ được n trong thanh ghi và tối ưu cả vòng lặp như một khối liền.
Ứng dụng thực tế
Getter/setter nhỏ gần như miễn phí — nhờ inline. Viết func (p Point) X() int { return p.x } không chậm hơn truy cập trực tiếp p.x, vì compiler inline nó (cost ~4). Đừng ngại đóng gói bằng phương thức nhỏ vì sợ chi phí gọi; cost model đảm bảo chúng biến mất.
Giữ hàm ở đường nóng đủ nhỏ để inline. Nếu một hàm trong vòng lặp nóng vượt ngân sách 80 (thân quá lớn, gọi nhiều hàm khác), tách phần nóng ra một hàm con nhỏ có thể giúp nó được inline. Kiểm bằng -gcflags=-m xem hàm của bạn có "can inline" không.
Cẩn thận defer trong hàm nhỏ ở đường nóng. Một defer — dù rẻ về mặt runtime trong Go hiện đại — chặn hẳn inline. Trong một accessor siêu nóng, thay defer mu.Unlock() bằng unlock tường minh (khi an toàn) có thể mở lại inline. Chỉ làm khi profiler chỉ ra và bạn giữ được tính đúng đắn.
Đánh đổi cần cân nhắc
Inline nhiều làm code phình to (I-cache). Dán thân hàm vào mọi chỗ gọi làm mã máy lớn hơn, có thể gây áp lực lên instruction cache. Ngân sách 80 là điểm cân bằng của Go giữa tốc độ và kích thước — đừng cố ép inline hàm lớn bằng cách chia nhỏ vô tội vạ, có thể phản tác dụng.
Đừng vi tối ưu inline mà chưa đo. Chênh lệch 2,7x nghe lớn nhưng là trên một hàm cực nhỏ trong vòng cực nóng; ở code thường, chi phí gọi bị lấn át bởi công việc thật. Chỉ can thiệp inline khi pprof chỉ ra một hàm nhỏ bị gọi hàng trăm triệu lần và không được inline.
-gcflags=-l để so sánh, không để dùng thật. Tắt inline giúp gỡ lỗi và đo baseline, nhưng đừng build production với nó — bạn mất một trong những tối ưu quan trọng nhất. Ngược lại, không có cờ chuẩn để ép inline một hàm; cách duy nhất là giữ nó dưới ngân sách và tránh cấu trúc chặn cứng.
Ba ý mang về
- Go quyết định inline bằng cost model với ngân sách 80: đo thật
congcost 4,tongcost 16,max3cost 21 đều được inline; xem cost và lý do bằnggo build -gcflags=-m=2. - Hai loại chặn: chặn cứng (
defer,recover,goto— đo thậtcoDeferbị "unhandled op DEFER" bất kể cost) và chặn mềm (vượt ngân sách,goiPhucost 120 > 80) — cẩn thận đặtdefertrong hàm nhỏ ở đường nóng. - Lợi ích của inline không chỉ là bỏ lệnh CALL: đo thật inline nhanh hơn noinline ~2,7 lần (0,257 so 0,709 ns) vì sau khi dán vào, compiler mở khoá tối ưu xuyên ranh giới — gập hằng, loại mã chết, giữ giá trị trong thanh ghi.
Phần sau ta xem mặt đối lập — cách chủ động chặn inline và vì sao đôi khi bạn muốn thế: Phần sau mổ xẻ chỉ thị //go:noinline — khi nào nó cần thiết cho benchmark đúng, cho pprof đọc được, và các directive compiler khác.