登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go benchmark 怎样避免编译器优化掉被测代码

来源:17golang原创

时间:2026-10-14 04:28:11 324浏览 收藏

Go benchmark 最容易被忽略的一类问题,是循环看起来执行了很多次,实际测到的却不是目标函数。典型代码把返回值赋给局部变量后立即丢弃,编译器发现这段计算没有可观察副作用,就可能把它简化甚至删除。解决办法是:Go 1.24 及以后优先使用精确写法 for b.Loop();旧版本继续使用 b.N 时,则把返回值汇总到包级变量。

要点速览
  • b.Loop() 的条件必须原样写在 for 中,循环体内的调用结果会被 testing 保活。
  • 旧式 b.N 不能只把结果放进循环内的临时变量,使用包级 sink 才能让计算保持可观察。
  • 输入准备放在计时前;旧式写法用 ResetTimer,多次运行再用 benchstat 比较。

先判断:基准循环是不是只剩空调用

假设要测一个返回整数的哈希或解析函数,下面的写法表达了“我关心函数耗时”,但没有表达“返回值必须留下来”。如果函数没有 I/O、全局写入或其他可见副作用,优化器就有机会认为调用结果不影响程序。

package demo

import "testing"

func BenchmarkHash(b *testing.B) {
	for i := 0; i 

这里的关键不是“每次都要打印结果”。打印会改变被测对象;正确做法是保存结果,但不要把日志、分配或锁竞争混进计时区域。

用 b.Loop 保住循环体的工作量

当前 testing 文档推荐新基准使用 B.Loop。它不只是把 b.N 换成另一种循环:在 for b.Loop() { ... } 的大括号内,函数调用的参数、返回值和赋值变量会被保活,避免被测调用被完全优化掉。条件必须精确写成 b.Loop(),不要包一层自定义函数或改成等价表达式。

Go testing.B 与 b.Loop、被测函数返回值和编译器保活边界的静态结构说明图
图1:b.Loop 结构说明图,展示循环体的计时范围与结果保活关系。
func BenchmarkHashLoop(b *testing.B) {
	input := "go benchmark"
	for b.Loop() {
		// b.Loop 会让循环体内的调用和返回值保持可观察。
		fastHash(input)
	}
}

func BenchmarkHashLoopUsed(b *testing.B) {
	input := "go benchmark"
	for b.Loop() {
		// 显式接住结果,便于表达测试意图和后续扩展统计。
		got := fastHash(input)
		_ = got
	}
}

第二个示例里的 _ = got 主要是说明意图;在支持 b.Loop 的 Go 版本中,真正重要的是循环条件和循环体位置。不要把 b.Loop 与 for i := 0; i 同时套在一个 benchmark 里。

旧式 b.N 写法为什么需要结果汇总

如果项目还要兼容没有 B.Loop 的 Go 版本,可以保留 b.N,但让结果流向包级变量。这样结果具有跨函数调用仍可观察的存储位置,编译器不能把整段计算简单视作“完全没用”。这个 sink 不应在并行基准之间共享可变状态,也不要在循环里打印。

Go 旧式 b.N 基准中输入、目标计算、包级结果汇总与计时边界的静态结构说明图
图2:结果汇总结构说明图,展示旧式 b.N 基准如何保留被测计算。
package demo

import "testing"

var benchmarkSink uint64

func BenchmarkHashN(b *testing.B) {
	input := "go benchmark"
	b.ResetTimer()
	for i := 0; i 

把计时边界和结果可信度一起复查

基准修好后,先只比较同一台机器、同一输入和同一编译配置下的结果。b.Loop 会自动处理迭代与计时;b.N 版本则要确认循环确实执行了 b.N 次,且 setup 没有被意外算进耗时。

检查点正确做法常见误区
循环条件新代码写成 for b.Loop()与 b.N 双重循环,或封装后再调用
结果去向Loop 内保活;旧式写法用包级 sink只赋给循环内临时变量后丢弃
输入准备计时前构造,旧式写法随后 ResetTimer把 make、读文件和解析初始化混在循环中
重复运行go test -run '^$' -bench BenchmarkHash -count=5 后用 benchstat只看一次 ns/op 就下结论
# 只运行目标 benchmark,重复采样以减少偶然抖动。
go test -run '^$' -bench '^BenchmarkHash' -count=5

# 对两组已保存的基准结果做统计比较。
benchstat before.txt after.txt

命令中的正则只负责筛选测试名;它不能证明实现一定没有被优化。最终仍要回到代码检查:目标调用是否位于计时循环内、返回值是否有存活路径、setup 是否被排除、输入规模是否一致。

相关问题

b.Loop 能不能和 b.N 一起使用?

不能。一个 benchmark 选择其中一种循环方式即可;新代码优先使用 b.Loop,旧代码才继续按 b.N 编写。

为什么不建议在 benchmark 里 fmt.Println 结果?

输出会带来格式化、系统调用和锁等额外成本,改变被测路径。用包级 sink 或 b.Loop 的保活语义即可保留结果。

ResetTimer 什么时候需要?

旧式 b.N benchmark 在循环前做了昂贵初始化时使用它;如果初始化本身就是要测的对象,则不要为了好看而排除它。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>