Bài interface internals cho thấy gọi phương thức qua interface tốn một lần tra itab và một nhảy gián tiếp — chậm hơn gọi trực tiếp. Nhưng có một tối ưu compiler làm mờ ranh giới đó: devirtualization. Khi compiler chứng minh được kiểu động cụ thể sau một interface lúc biên dịch, nó thay dispatch động bằng lời gọi trực tiếp — rồi thường inline luôn, khiến toàn bộ lời gọi biến mất. Bài này đo hiện tượng đó bằng assembly và benchmark, và làm rõ khi nào nó xảy ra.
Cơ chế: chứng minh kiểu rồi gọi thẳng
Ý tưởng đơn giản: nếu compiler biết chắc h là Vuong (không phải một kiểu nào đó thỏa Hinh), thì h.Dientich() không cần dispatch động — nó gọi thẳng Vuong.Dientich. Mẫu điển hình là gán một kiểu cụ thể ngay trước khi gọi:
func dungKieuCuThe() float64 {
var h Hinh = Vuong{3} // compiler BIẾT h là Vuong
return h.Dientich() // viết như gọi interface, nhưng devirtualize thành Vuong.Dientich
}
Ngược lại, khi kiểu động thay đổi lúc chạy — ví dụ lấy từ một slice trộn nhiều kiểu — compiler không chứng minh được, phải giữ dispatch động:
var mau = []Hinh{Vuong{2}, Tron{3}}
func khongBiet() float64 {
i = (i + 1) & 1
return mau[i].Dientich() // kiểu động đổi mỗi vòng → dispatch động thật
}

Hình 1: Devirtualize được khi kiểu cụ thể biết tĩnh (gán Vuong{3} rồi gọi); không được khi kiểu động đổi lúc chạy (mau[i]). Xác nhận qua -m và assembly.
Bằng chứng: dispatch biến mất trong assembly
Chạy -gcflags=-m in ra chính xác quyết định:
devirtualizing x.Gia to A2 // thay dispatch động bằng gọi trực tiếp
inlining call to A2.Gia // rồi dán thẳng phương thức vào
Và assembly của hàm (với Gia() trả x+1) chỉ còn:
main.devirtDuoc STEXT
ADD $1, R0, R0 // chỉ còn x + 1
RET
Không nạp itab, không CALL gián tiếp. Hai bước — devirtualize rồi inline — gộp một lời gọi phương thức qua interface thành đúng một phép cộng. Đây là chuỗi tối ưu đẹp: interface (linh hoạt runtime) mà khi kiểu biết tĩnh thì không trả giá gì so với gọi trực tiếp.
Đo thật: nhanh hơn ~3,8 lần
Benchmark so devirtualize được với dispatch động thật (dùng phương thức trả int và tích lũy integer để lộ chi phí dispatch, tránh bị latency phép cộng float che):

Hình 2: Devirt2 (kiểu cụ thể biết tĩnh) 0,4707 ns so với Dynamic2 (kiểu đổi lúc chạy) 1,796 ns — nhanh hơn ~3,8 lần. Bảng điều kiện để devirtualize xảy ra.
- Devirt2 (kiểu cụ thể biết tĩnh): 0,4707 ns — devirtualize + inline.
- Dynamic2 (kiểu đổi lúc chạy): 1,796 ns — dispatch động thật (tra itab + nhảy gián tiếp).
Nhanh hơn ~3,8 lần khi devirtualize được. Chênh lệch chính là chi phí dispatch động mà devirtualization loại bỏ.
Điều kiện để devirtualize xảy ra
Devirtualization chỉ xảy ra khi compiler chứng minh được kiểu động, thường trong phạm vi một hàm:
- Được:
var h Iface = KieuCuThe{...}rồih.M()ngay sau — kiểu tĩnh biết. - Được: gán một giá trị kiểu cụ thể vào biến interface rồi gọi trong cùng hàm.
- Không: slice/map trộn nhiều kiểu (
mau[i].M()), kiểu động thay đổi lúc chạy. - Thường không: tham số interface truyền từ hàm khác — kiểu động bị ẩn khỏi tầm nhìn của compiler.
Ứng dụng thực tế
Đừng "tối ưu sớm" bằng cách né interface. Nếu bạn dùng interface nhưng kiểu cụ thể thường biết tĩnh tại chỗ gọi, compiler tự devirtualize — bạn được cả tính trừu tượng lẫn tốc độ. Viết code sạch với interface trước, để compiler lo.
Devirtualization giải thích benchmark interface "quá nhanh". Nếu benchmark một lời gọi interface cho kết quả gần bằng gọi trực tiếp, rất có thể compiler đã devirtualize vì kiểu cụ thể biết tĩnh trong vòng lặp benchmark — không phản ánh chi phí dispatch thật ở code production nơi kiểu động thay đổi. Muốn đo dispatch thật, dùng slice trộn kiểu (như Dynamic2).
PGO (profile-guided optimization) mở rộng devirtualization. Từ Go 1.21, PGO có thể devirtualize cả những lời gọi mà profile cho thấy hầu hết thời gian là một kiểu cụ thể — chèn một kiểm tra kiểu rồi gọi trực tiếp cho nhánh nóng. Đây là devirtualization "suy đoán", vượt qua giới hạn "phải chứng minh tĩnh".
Đánh đổi cần cân nhắc
Devirtualization là quà tặng, không phải bảo đảm. Bạn không điều khiển trực tiếp được nó (không có directive //go:devirtualize). Nó phụ thuộc compiler chứng minh được kiểu — mà điều đó dễ vỡ: thêm một nhánh khiến kiểu không còn chắc chắn là mất devirtualize. Đừng thiết kế hiệu năng dựa vào nó ở code quan trọng; nếu cần chắc chắn không dispatch, dùng kiểu cụ thể hoặc generics.
Generics cho đa hình không-dispatch một cách tường minh. Nếu bạn muốn đảm bảo không có dispatch động (thay vì hi vọng devirtualize), generics giữ kiểu tĩnh xuyên suốt — compiler sinh mã riêng cho từng kiểu, không itab. Đây là lựa chọn chắc chắn hơn khi kiểu biết lúc biên dịch; interface + devirtualization là cho khi bạn cần linh hoạt runtime nhưng may mắn kiểu biết tĩnh tại chỗ.
Dispatch động không phải kẻ thù. ~1,8 ns cho một lời gọi động là rất rẻ tuyệt đối. Devirtualization đáng để ý ở vòng cực nóng gọi hàng trăm triệu lần, nhưng ở code thường, tính linh hoạt của interface đáng giá hơn ~1,3 ns tiết kiệm được. Đo trước khi tối ưu.
Ba ý mang về
- Devirtualization thay lời gọi interface bằng lời gọi trực tiếp khi kiểu động biết tĩnh, rồi thường inline luôn: đo thật, assembly của một lời gọi interface co xuống còn một lệnh
ADD(không itab, khôngCALL) — xác nhận bằng-gcflags=-min dòng "devirtualizing". - Devirtualize được nhanh hơn dispatch động ~3,8 lần (0,47 so 1,80 ns): chênh lệch chính là chi phí tra itab + nhảy gián tiếp mà nó loại bỏ — nhưng chỉ xảy ra khi compiler chứng minh được kiểu (thường trong một hàm, sau phép gán kiểu cụ thể).
- Devirtualization là quà tặng dễ vỡ, không phải bảo đảm: không có directive điều khiển, và thêm một nhánh có thể làm mất nó — nếu cần chắc chắn không dispatch, dùng generics; interface + devirtualize là cho khi cần linh hoạt runtime mà kiểu may mắn biết tĩnh.
Phần sau ta xem chính công cụ compiler dùng để làm mọi tối ưu này — biểu diễn trung gian: Phần sau mổ xẻ SSA (Static Single Assignment) — cách xem biểu diễn trung gian của Go, đọc các pha tối ưu, và hiểu compiler "suy nghĩ" thế nào về code của bạn.