编译器替你 free:GOEXPERIMENT=runtimefreegc 的插入条件、16 字节阈值与收益边界
项目信息
- 设计文档:(2025 年 11 月发布)
- Issue:golang/go#74299 runtime, cmd/compile: add runtime.freegc and runtime.freegcTracked to reduce GC work
- 背景阅读:Tony Bai《Go 1.28 路线图首度曝光》
这是 Go 1.28 周期的规划议题之一,处于实验阶段,不代表最终 API。
它想省掉哪一段工作
常规路径下,一块堆内存变成垃圾之后,要等下一轮 GC 通过可达性分析确认不可达,才可能被回收。栈帧中的 liveness 分析能让 GC 扫描时跳过已经死亡的栈槽,但回收和归还给分配器仍要等 GC 周期。
runtime.freegc 的思路是把这个动作提前:编译器在能够确定某块堆内存“确实不会再被使用”的位置,插入调用 runtime.free,由运行时把这块内存提前归还给分配器,后续 GC 需要扫描的对象也就少了。原型在 GOEXPERIMENT=runtimefreegc 下实现,配套的追踪变体是 runtime.freegcTracked。
与 runtime.GC、debug.FreeOSMemory 的区别
| 粒度 | 触发者 | 开销 | |
|---|---|---|---|
runtime.GC() | 全局 | 用户代码 | 一整轮 GC |
debug.FreeOSMemory() | 全局,归还 OS | 用户代码 | 一轮 GC + 归还系统内存 |
runtime.free | 单个对象 | 编译器在静态可证明的死点插入 | 函数调用、位图查询、堆记账 |
前者是“现在就整体回收一遍”,后者是“这一块我已经知道它死了,顺手交回去”。
编译器能证明什么
会插入死点判断的情形:
- 对象在同一函数内分配,之后没有再逃逸到别处,最后一次使用之后再无读取;
- 循环体内的临时对象,每轮结束后被下一轮的赋值覆盖;
- 被重新赋值覆盖的局部变量。
不会插入的:跨 goroutine 传递的、存在别名或经过 unsafe 的、通过接口或反射持有的、编译器无法在静态层面排除再次访问的。这些对象会保持原有路径:等待 GC 按可达性分析回收。
一个典型的死点位置:
func process(data []byte) int {
scratch := make([]byte, 1/dev/null && GOEXPERIMENT=runtimefreegc gotip version
先确认逃逸行为,不然基准测的是栈分配:
GOEXPERIMENT=runtimefreegc gotip build -gcflags=-m ./...
基准对比要分别构建两个二进制,GOEXPERIMENT 不会在运行期生效:
gotip test -run='^$' -bench=. -benchmem -count=10 > off.txt
GOEXPERIMENT=runtimefreegc gotip test -run='^$' -bench=. -benchmem -count=10 > on.txt
benchstat off.txt on.txt
基准用例本身要构造成“编译器能证明死”的形状,同时保证对象真的在堆上:
package freegcbench
import "testing"
// 每轮分配一个大对象,上一轮的对象在下一轮开始时已不可达
func churn(n int) int {
sum := 0
for i := 0; i < n; i++ {
buf := make([]byte, 64<<10)
buf[i%len(buf)] = 1
sum += int(buf[i%len(buf)])
}
return sum
}
func BenchmarkChurn(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
churn(1000)
}
}
对 churn 跑一次 -gcflags=-m,确认 buf 逃逸到堆、没有被完全优化掉;必要时加大对象尺寸或引入可观测的副作用。GC 行为用 GODEBUG=gctrace=1 看周期数和堆增长,和 -benchmem 的 B/op、allocs/op 一起读。要把阈值的影响单独隔离出来,可以构造尺寸梯度的用例(比如 8/16/32/64/1024 字节),观察收益在哪个尺寸开始由负转正。
同一周期里还发生了什么
Green Tea GC 在 1.25/1.26 引入,按 block 扫描改善 cache locality;1.28 周期继续打磨大规模堆下的可扩展性。runtime.freegc 走的是另一条路:不改进扫描方式,而是减少需要扫描的对象。参与人有 Keith Randall(编译器团队)和社区贡献者 thepudds。
状态仍然是实验:接口名、阈值、插入策略都可能变。想要在生产上依赖它之前,先在真实负载上跑基准。