Go编译器会优化掉未赋值的Benchmark计算结果,因其判定该计算对程序行为无影响;应使用runtime.KeepAlive作用于最终结果并置于计时区间末尾,或赋值给被间接引用的全局变量。

为什么 Benchmark 函数里的计算结果不赋值会导致被编译器优化掉
Go 编译器(特别是 gc)在构建测试二进制时默认启用全量优化(-gcflags="-l -m" 可见),只要它判定某段代码的输出对程序行为无影响,就会直接删掉——包括你精心写的循环、哈希计算、JSON 序列化等。典型现象是:明明写了 100 万次 sha256.Sum256,但基准测试耗时恒定在几纳秒,B.N 却跑到了最大值,说明整个循环体被优化为空。
用 blackhole 或 B.ReportAllocs() 不解决根本问题
常见误区是以为调用 B.ReportAllocs() 或把结果丢给 fmt.Print 就能“防止优化”。其实这些只是副作用操作,编译器仍可能提前推导出“该值从未被读取”,进而消除上游计算。真正可靠的方式是让结果参与一个无法被静态分析绕过的、必须执行的路径:
- 将关键中间值赋给全局变量(如
var result sha256.Sum256),并确保该变量在func init()或其他非测试函数中被间接引用(否则链接器可能丢掉) - 更推荐:用
runtime.KeepAlive显式告知编译器“这个值在此处必须存活” - 对简单类型(如
int、string),可传入B.StopTimer()/B.StartTimer()区间外的blackhole函数,但需确认该函数内联未被禁用(加//go:noinline)
runtime.KeepAlive 的正确用法和陷阱
这是 Go 官方推荐的防优化手段,但它不是“阻止编译器删除代码”,而是“阻止编译器提前回收值的内存”,所以必须作用于最终计算结果,并放在计时区间内最后一行:
func BenchmarkSha256(b *testing.B) {
data := make([]byte, 1024)
b.ResetTimer()
for i := 0; i < b.N; i++ {
sum := sha256.Sum256(data)
runtime.KeepAlive(sum) // ✅ 必须放在这里,且 sum 是最终结果
}
}
常见错误:
立即学习“go语言免费学习笔记(深入)”;
- 对中间变量调用
KeepAlive(如buf := make([]byte, 32); runtime.KeepAlive(buf)),但后续没用到buf→ 优化照旧 - 把
KeepAlive放在b.StopTimer()后 → 计时不包含它,但编译器仍可能优化掉前面的计算 - 传指针时未解引用(
runtime.KeepAlive(&sum))→ 实际保活的是地址,而非值本身,效果弱
验证是否真的没被优化:看汇编或加 -gcflags="-l -m"
最直接的方法是检查编译器日志。在运行 go test -bench=. -gcflags="-l -m" 2>&1 | grep -A5 "BenchmarkSha256" 时,如果看到类似 can inline BenchmarkSha256 或 deadcode 相关提示,大概率已失效。更稳妥的是生成汇编:
go tool compile -S -l -m=2 benchmark_test.go 2>&1 | grep -A10 "BenchmarkSha256"
观察循环体内是否还存在 CALL runtime.sha256block 类指令。没有?说明优化生效了。此时回退检查 KeepAlive 位置、变量作用域、以及是否意外触发了常量折叠(比如对固定字符串反复哈希)。
真正难缠的是编译器基于数据流分析做的跨函数优化——哪怕你用了 KeepAlive,如果整个计算链路能被证明“结果恒为零”或“与输入无关”,它依然会砍掉。这时候得靠人工拆分逻辑、引入不可预测输入(如 time.Now().UnixNano() 混入,仅用于验证)、或改用 unsafe.Pointer 强制打断分析链。不过那已经超出基准测试范畴了。










