後端工坊 2026 年 9 月 17 日

2026-09-17 — Go 1.27:小物件配置換成專用配置函式

primary=https://go.dev/blog/size-specialized-allocations

Go 1.27:小物件配置換成專用配置函式

Go Blog · 2026-09-16

小於 80 bytes 的堆積配置,從 Go 1.27 起不再統一走舊有的通用 mallocgc 路徑,而是直接呼叫編譯器產生的size-specialized配置函式。這項改動由 Go 團隊工程師 Michael Matloob 於 2026 年 9 月 16 日在 Go Blog 公開說明。官方數字顯示,小物件配置本身最多快 20% 到 30%,配置密集的程式整體則可以快到 1%。

背景:通用配置路徑做了什麼

Go 的堆積配置都是由 runtime 的 mallocgc 函式處理:只要編譯器判斷一個物件會逃逸到堆積、或需要動態配置,就會插入一段呼叫 newobject 的程式碼。newobject 只是個包裝函式,把物件大小與是否含指標這兩項資訊取出後轉交給 mallocgc,這兩項資訊幾乎決定了配置器要做的所有事情

配置器把大小切成一段一段的「size class」,例如 size class 3 代表 17 到 24 bytes:不管你要配置 17 bytes 還是 24 bytes,都會拿到同一份 24 bytes 大小的物件。是否含指標則決定要從哪一組「span」(也就是這些固定大小物件的空閒串列)裡取,因為 GC 需要對含指標與不含指標的記憶體做不同的記帳,這兩項資訊合起來構成span class,定義為 sizeClass<<1 | noPointers

舊路徑的問題在於 mallocgc 是一個完全通用的函式:不管配置多大、含不含指標,都要在執行期動態算出 span class、逐一檢查每一種邊界情況,再呼叫通用組合語言函式 memclrNoHeapPointers 把記憶體歸零。對配置量最大的一批小物件(例如 interface 值、字串、slice 常見的 16、24 bytes 配置)而言,這些為了因應各種情況而存在的判斷與函式呼叫,反而變成不成比例的額外負擔

核心改動:size-specialized 配置函式怎麼運作

Go 1.27 的做法,是替每一個 span class 都產生一份特化版的 mallocgc 變體,例如負責 size class 3、不含指標配置的函式就叫mallocgcSmallNoScanSC3。這些函式設計得盡量精簡,遇到處理不了的邊界情況(例如 GC 正在進行中)就會退回原本的通用路徑,正確性因此完全不受影響。

當編譯器在編譯期就已經知道配置大小與是否含指標,例如配置欄位數固定的 struct,就會直接插入對特化函式的呼叫,跳過 newobject。如果編譯期無法確定大小(像是長度不固定的 slice),呼叫仍然會落在 mallocgc 上,由它在執行期判斷該用哪個特化函式再動態呼叫過去,這代表特化函式的效能必須快到連補上這層動態呼叫的開銷都划算

// 舊路徑:一律呼叫通用 mallocgc,所有事情都在執行期決定
newobject(size, hasPtr) -> mallocgc(size, hasPtr)
    -> 動態算出 span class
    -> 呼叫 memclrNoHeapPointers 做記憶體歸零
    -> 檢查並處理每一種邊界情況

// Go 1.27:編譯期已知大小與指標性時,直接呼叫特化函式
mallocgcSmallNoScanSC3(...)   // size class 3 = 17-24 bytes,不含指標
    -> span class 已知,略過計算
    -> 大小是常數,編譯器內嵌歸零指令
    -> 遇到特殊情況才 fallback 回 mallocgc
Size classRange
11-8 bytes
29-16 bytes
317-24 bytes
425-32 bytes
533-48 bytes
649-64 bytes
765-80 bytes

帶來最大效益的其實是記憶體歸零。當配置大小是常數,編譯器可以把「呼叫組合語言函式 memclrNoHeapPointers」直接換成內嵌的歸零指令,省下一次函式呼叫與幾個分支判斷,配置越小,這筆省下來的成本佔比就越高。除此之外,特化函式因為已經知道 span class,不必再動態計算;因為大小是常數,編譯器也能把指標位置的記帳工作進一步簡化,並手動內聯多個 helper 函式,把除錯旗標之類的少數情況搬進慢路徑,讓熱路徑更精簡。

為了不讓每個特化函式各自維護、逐漸寫壞走鐘,Go 團隊用標準庫 go/ast 搭配 golang.org/x/tools/go/ast/astutil 寫了一個程式碼產生器,共用邏輯只用一般 Go 程式碼寫一次、照樣接受型別檢查,再由產生器內聯進每個特化函式裡。80 bytes 這個門檻也是實測調校出來的結果:特化函式一多,執行檔會變大,更關鍵的是會占用指令快取(icache)空間,而 mallocgc 因為呼叫頻繁,原本就經常留在 icache 裡,一旦特化函式太多、擠不進 icache,反而會把使用者程式碼擠出去,抵銷掉原本的效益。這個功能原訂在 Go 1.26 推出,就是為了做這些調校才延後了一個版本。

影響範圍:誰會受益、rollout 要注意什麼

這項改動不需要改任何應用程式碼,只要用 Go 1.27 重新編譯就能拿到效益。獲益最明顯的是 16 bytes 與 24 bytes 這兩種配置,因為它們分別對應兩個、三個 64 位元欄位組成的常見結構,也就是 interface 值,以及字串、slice 這類在配置密集服務裡出現頻率極高的型別。

對於用 GOEXPERIMENT 控制 rollout 的團隊,如果懷疑新的配置路徑造成非預期行為,可以用 GOEXPERIMENT=nosizespecializedmalloc 整個關掉這項功能後重新建置來排除問題。Go 團隊表示這項改動不應該造成效能回歸,但仍保留這個逃生閘門,也建議把碰到的問題回報到 go.dev/issue/new。整體而言,配置密集程式的實測效益落在 1% 上下,數字看似不大,但對長期跑在高吞吐服務、配置又以小物件為主的程式來說,這是一筆幾乎零成本就能拿到的效能提升。

原始來源:Size-Specialized Memory Allocation - The Go Blog


End of article
0
Would love your thoughts, please comment.x
()
x