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)

Ảnh chụp đoạn mã Go nền tối minh hoạ inlining cost model và quyết định, compiler cho mỗi hàm một chi phí cost dưới ngưỡng ngân sách 80 thì được dán thẳng vào chỗ gọi vài cấu trúc chặn hẳn, một cost model đo bằng go build gcflags trừ m bằng 2 func cong a b int return a cộng b cost 4 inline func max3 a b c int cost 21 inline func tong xs int for range xs cost 16 inline Go 1.23 mỗi lệnh cộng vào cost toán tử khoảng 1 lời gọi hàm khác đắt hơn nhiều ngân sách mặc định là 80, hai cái gì chặn inline func coDefer int defer func rỗng cannot inline unhandled op DEFER return 1 func goiPhu int return phu cộng phu cannot inline cost 120 exceeds budget 80 gọi hàm noinline hai lần mỗi lời gọi cộng chi phí lớn chặn cứng defer recover goto closure phức tạp hàm quá lớn chặn mềm vượt ngân sách 80, ba vì sao inline nhanh hơn không chỉ bỏ chi phí gọi s cộng bằng binhphuong n với binhphuong x bằng x nhân x inline s cộng bằng n nhân n dán thẳng rồi tối ưu tiếp fold dead code noinline CALL binhphuong ranh giới gọi chặn mọi tối ưu xuyên hàm lợi ích lớn nhất của inline không phải tiết kiệm lệnh CALL mà mở khoá tối ưu xuyên ranh giới constant folding loại mã chết giữ giá trị trong thanh ghi, bốn xem điều chỉnh go build gcflags trừ m in can inline gcflags trừ m bằng 2 in cả cost cụ thể lý do chặn gcflags trừ l tắt inline để so sánh gỡ lỗi

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:

Ảnh chụp bảng kết quả đo thật nền tối cost model và tác động của inlining go build gcflags trừ m bằng 2 go test bench Go 1.23 arm64 10 core, cost của từng hàm ngân sách bằng 80 cong a b return a cộng b cost 4 inline tong xs có for range cost 16 inline max3 a b c hai if cost 21 inline coDefer có defer chặn unhandled op DEFER goiPhu gọi noinline 2 lần cost 120 chặn vượt budget 80 toán tử cộng khoảng 1 vào cost lời gọi hàm không inline cộng rất nhiều goiPhu tới 120 defer recover goto chặn cứng bất kể cost, tác động lên vòng nóng s cộng bằng binhphuong n Inline binhphuong 0,2570 0,2580 0,2565 ns NoInline go noinline 0,7094 0,7139 0,7040 ns inline nhanh hơn khoảng 2,7 lần chênh lệch không chỉ đến từ việc bỏ lệnh CALL khi đã dán vào compiler tối ưu tiếp giữ trong thanh ghi gập hằng điều ranh giới gọi chặn mất, cốt lõi cost model mỗi hàm một điểm chi phí ngân sách 80 inline khi cost nhỏ hơn 80 và không có defer recover goto lợi ích thật mở khoá tối ưu xuyên hàm không chỉ bỏ CALL đo thật 0,257 ns inline vs 0,709 ns noinline khoảng 2,7x

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ề

  1. Go quyết định inline bằng cost model với ngân sách 80: đo thật cong cost 4, tong cost 16, max3 cost 21 đều được inline; xem cost và lý do bằng go build -gcflags=-m=2.
  2. Hai loại chặn: chặn cứng (defer, recover, goto — đo thật coDefer bị "unhandled op DEFER" bất kể cost) và chặn mềm (vượt ngân sách, goiPhu cost 120 > 80) — cẩn thận đặt defer trong hàm nhỏ ở đường nóng.
  3. 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.