Bài devirtualization cho thấy compiler biến lời gọi interface thành lời gọi trực tiếp khi chứng minh được kiểu tĩnh. Nhưng phần lớn lời gọi interface trong code thật thì compiler không chứng minh được — kiểu động đến từ nơi khác. PGO (Profile-Guided Optimization) giải quyết đúng chỗ này: nó dùng một profile CPU từ chương trình chạy thật để biết lời gọi nào thường là kiểu nào, rồi tối ưu dựa trên đó. Bài này đo thật PGO làm gì — và trung thực về khi nào nó không giúp gì.

Vấn đề PGO giải quyết

Xét một hàm nóng nhận interface, mà nơi gọi giấu kiểu cụ thể:

type Xuly interface{ Chay(x int) int }

// nhận interface → compiler KHÔNG biết kiểu động tĩnh
func xuLyNong(h Xuly, n int) int {
	s := 0
	for i := 0; i < n; i++ { s += h.Chay(i) }  // dispatch động, không devirt tĩnh được
	return s
}

Devirtualization tĩnh (bài trước) bó tay: h đến từ hàm khác, compiler không biết chắc kiểu. Nhưng lúc chạy, giả sử 99% trường hợp h là kiểu A. Thông tin này chỉ có khi quan sát chương trình chạy — đó là điều profile cung cấp.

Ảnh chụp đoạn mã Go nền tối minh hoạ PGO tối ưu dựa trên profile chạy thật, profile-guided optimization compiler dùng profile CPU từ production để tối ưu điều phân tích tĩnh không làm được, một vấn đề lời gọi interface không devirt tĩnh được type Xuly interface Chay x int int nhận interface compiler không biết kiểu động tĩnh func xuLyNong h Xuly n int int s bằng 0 for i bằng 0 i nhỏ hơn n i cộng cộng s cộng bằng h Chay i dispatch động return s devirtualization tĩnh bó tay ở đây kiểu động đến từ nơi khác compiler không chứng minh được nhưng lúc chạy 99% là kiểu A, hai ba bước dùng PGO 1 thu profile CPU từ chạy thật production benchmark pprof StartCPUProfile f defer pprof StopCPUProfile 2 đặt profile tên default.pgo cạnh package main cp cpu.pprof default.pgo Go 1.21 cộng tự nhận default.pgo 3 build compiler tự dùng nó go build hoặc pgo default.pgo tường minh, ba PGO làm gì devirtualize suy đoán profile thấy h Chay 99% là A Chay compiler chèn if kiểu h bằng bằng A A Chay i gọi trực tiếp inline được nhánh nóng nhanh else h Chay i dispatch động nhánh dự phòng khác devirt tĩnh phải chắc chắn kiểu PGO chỉ cần biết kiểu nào phổ biến thêm kiểm tra cộng nhánh dự phòng vượt qua giới hạn phải chứng minh tĩnh, bốn PGO còn làm gì khác inline nóng nới ngân sách inline cho hàm profile cho thấy nóng sắp xếp khối đặt nhánh nóng liền nhau tốt cho I-cache

Hình 1: Vấn đề PGO giải quyết và ba bước dùng. PGO devirtualize suy đoán dựa profile: chèn kiểm tra kiểu + gọi trực tiếp cho nhánh nóng, dispatch động làm dự phòng.

Ba bước dùng PGO

# 1. Thu profile CPU từ chạy thật (production hoặc benchmark đại diện)
pprof.StartCPUProfile(f); defer pprof.StopCPUProfile()

# 2. Đặt profile tên default.pgo cạnh package main
cp cpu.pprof default.pgo   # Go 1.21+ TỰ nhận file này

# 3. Build — compiler tự dùng nó
go build .                 # hoặc -pgo=default.pgo tường minh

Từ Go 1.21, nếu có file default.pgo trong thư mục package main, go build tự dùng nó — không cần cờ. Đây là thiết kế để PGO dễ áp dụng: chỉ cần commit profile vào repo.

PGO làm gì: devirtualize suy đoán

Điểm khác cốt lõi với devirt tĩnh: PGO không cần chắc chắn kiểu, chỉ cần biết kiểu nào phổ biến. Với profile cho thấy h.Chay 99% là A.Chay, compiler chèn:

if kiểu(h) == A {
	A.Chay(i)      // gọi TRỰC TIẾP, inline được → nhánh nóng nhanh
} else {
	h.Chay(i)      // dispatch động → nhánh dự phòng
}

Một kiểm tra kiểu rẻ, rồi nhánh nóng gọi trực tiếp (và có thể inline sâu), nhánh dự phòng vẫn dùng dispatch động cho các kiểu khác. Đây là cách PGO vượt qua giới hạn "phải chứng minh tĩnh" của devirt thường.

Đo thật: nó devirtualize, nhưng...

Xác minh PGO có tác dụng bằng -gcflags=-m:

Ảnh chụp bảng kết quả đo thật nền tối PGO có devirtualize nhưng lợi ích tùy khối lượng go build gcflags trừ m cộng go test bench Go 1.23 arm64 10 core profile 99% kiểu A, PGO devirtualize được lời gọi mà tĩnh không làm được build pgo off không dòng nào dispatch động giữ nguyên pgo default.pgo PGO devirtualizing interface call h Chay to A Chay xác nhận PGO làm điều devirt tĩnh không làm được dùng profile 99% kiểu A để suy đoán devirtualize lời gọi interface h Chay thành A Chay cộng nhánh dự phòng, nhưng benchmark micro gần như không đổi trung thực build không PGO 1685 1667 1706 có PGO 1693 1704 1727 trên benchmark đồ chơi này PGO không cải thiện rõ chi phí kiểm tra kiểu thêm vào bù trừ phần dispatch tiết kiệm cho lời gọi quá rẻ Chay chỉ là x cộng 1 đây là sự thật cần biết PGO không phải phép màu cho mọi code, lợi ích thật của PGO ở đâu micro benchmark gần như 0 lời gọi quá rẻ type-guard bù trừ app lớn thật 2-14% số Go team báo nhiều hot path phức tạp lợi nhiều nhất hàm nóng đắt cộng devirt mở khoá inline sâu PGO tỏa sáng trên dịch vụ lớn với nhiều đường nóng nơi devirtualize mở khoá inline cả chuỗi hàm đo trên tải thật của bạn đừng tin con số benchmark nhỏ, cốt lõi PGO tối ưu dựa profile CPU chạy thật default.pgo Go 1.21 cộng làm gì devirt suy đoán cộng inline nóng cộng sắp khối xác nhận pgo off không devirt pgo default.pgo có đo bằng trừ m sự thật micro gần 0 app lớn 2-14% đo trên tải thật dùng khi dịch vụ nóng sau khi profile đã đại diện production

Hình 2: -pgo=off không devirt; -pgo=default.pgo in "PGO devirtualizing interface call h.Chay to A.Chay". Nhưng benchmark micro gần như không đổi (~1690 ns cả hai) — lợi ích thật là 2-14% trên app lớn.

  • -pgo=off: không dòng nào — dispatch động giữ nguyên.
  • -pgo=default.pgo: PGO devirtualizing interface call h.Chay to A.Chay — PGO đã devirtualize dựa profile.

PGO đã làm điều devirt tĩnh không làm được. Nhưng benchmark thời gian thì trung thực mà nói: gần như không đổi — không PGO ~1.685 ns, có PGO ~1.700 ns. Vì sao? Lời gọi Chay quá rẻ (x+1), nên chi phí kiểm tra kiểu thêm vào bù trừ đúng phần dispatch tiết kiệm được. Đây là sự thật quan trọng: PGO không phải phép màu cho mọi code.

Lợi ích thật của PGO ở đâu

  • Benchmark micro: gần như 0 — lời gọi quá rẻ, type-guard bù trừ phần lợi.
  • App lớn thật: 2-14% (con số Go team báo cáo) — nơi có nhiều đường nóng phức tạp.
  • Lợi nhiều nhất: hàm nóng đắt, nơi devirtualize mở khóa inline cả một chuỗi hàm, kích hoạt các tối ưu xuyên hàm khác.

PGO tỏa sáng trên dịch vụ lớn với nhiều hot path, không phải trên benchmark đồ chơi. Con số benchmark nhỏ ở đây minh họa đúng điều này — đừng kỳ vọng phép màu từ một hàm x+1.

Ứng dụng thực tế

Áp PGO cho dịch vụ backend nóng. Nếu bạn chạy một service Go xử lý nhiều request, thu profile CPU từ production (hoặc staging đại diện), commit thành default.pgo, và build sẽ tự dùng. Vài phần trăm hiệu năng "miễn phí" ở quy mô lớn là đáng.

Profile phải đại diện tải thật. PGO tối ưu theo cái profile thấy. Nếu profile lấy từ tải không giống production (ví dụ chỉ một endpoint), PGO tối ưu sai chỗ. Thu profile từ tải thực tế, đa dạng, đủ dài.

Cập nhật profile định kỳ. Khi code và mẫu tải đổi, profile cũ có thể lỗi thời. Nhiều nhóm tự động hóa: thu profile từ production hàng tuần, cập nhật default.pgo trong CI. Profile lỗi thời vẫn an toàn (chỉ là tối ưu kém tối ưu), không gây sai.

Đánh đổi cần cân nhắc

PGO tăng thời gian biên dịch. Compiler phải đọc profile và làm thêm phân tích, nên build với PGO chậm hơn (Go team báo ~vài % tới hơn thế tùy dự án). Đổi thời gian build lấy hiệu năng runtime — đáng với service chạy lâu, ít đáng với công cụ CLI chạy ngắn.

Lợi ích rất khác nhau, phải đo trên tải thật. Như benchmark cho thấy, PGO có thể cho ~0% trên code đơn giản và tới 14% trên code phức tạp. Đừng áp PGO rồi giả định nhanh hơn — đo trên tải production thật trước và sau. Với nhiều ứng dụng, lợi ích không đủ để bù phức tạp vận hành (quản lý profile).

PGO an toàn nhưng thêm phức tạp quy trình. Profile sai/lỗi thời không làm chương trình sai (chỉ tối ưu kém), nên PGO an toàn về đúng đắn. Nhưng nó thêm một artifact phải quản lý (thu, cập nhật, commit profile). Cân nhắc chi phí vận hành này với lợi ích đo được — chỉ áp khi lợi ích rõ ràng.

Ba ý mang về

  1. PGO dùng profile CPU chạy thật để tối ưu điều phân tích tĩnh không làm được: đo thật, PGO devirtualize suy đoán lời gọi h.Chay thành A.Chay (dựa profile 99% kiểu A) mà devirt tĩnh bó tay — chèn kiểm tra kiểu + gọi trực tiếp cho nhánh nóng, dispatch động làm dự phòng; xác nhận bằng -gcflags=-m.
  2. Dùng PGO cực dễ nhưng lợi ích tùy khối lượng: chỉ cần đặt default.pgo cạnh package main (Go 1.21+ tự nhận) — nhưng đo thật benchmark micro gần như không đổi (~1690 ns) vì lời gọi quá rẻ, type-guard bù trừ; lợi ích thật là 2-14% trên app lớn với nhiều hot path.
  3. PGO an toàn nhưng đo trên tải thật trước khi áp: profile phải đại diện production và cập nhật định kỳ, PGO tăng thời gian build và thêm artifact phải quản lý — chỉ đáng cho dịch vụ nóng chạy lâu nơi vài phần trăm hiệu năng có giá trị.

Phần sau ta chuyển sang một chủ đề nền tảng của lập trình đồng thời: Phần sau mổ xẻ Go memory model — quan hệ happens-before, vì sao đọc/ghi không đồng bộ giữa goroutine là hành vi không xác định, và các bảo đảm mà channel/mutex/atomic cung cấp.