go基准测试需用b.run为每组输入创建独立子测试,避免顶层循环;数据构造应放在b.resettimer()前以确保计时准确,过多子测试可分文件管理。

Go benchmark 怎么用 Benchmark 函数跑多个数据集
Go 的基准测试不支持直接传参,所以得靠循环 + b.Run 拆成子测试。别在 BenchmarkXxx 顶层写 for 循环测不同输入——那样只会被当成一次运行,go test -bench 统计的是总耗时,看不出单组差异。
正确做法是用 b.Run 显式定义子测试名,让每组输入独立计时、单独输出结果:
func BenchmarkParseInt(b *testing.B) {
cases := []struct {
name string
s string
}{
{"small", "42"},
{"large", "1234567890123456789"},
{"hex", "0xFF"},
}
for _, c := range cases {
b.Run(c.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = strconv.ParseInt(c.s, 0, 64)
}
})
}
}
-
b.Run名字必须唯一,否则后一个会覆盖前一个的统计 - 子测试里仍要写
for i := 0; i ,不能省略——<code>b.N是 Go 自动调整的迭代次数,不是固定值 - 如果某组输入太慢(比如解析超长字符串),它会拖慢整个
BenchmarkXxx的预热和采样,建议拆到独立的BenchmarkXxxSlow里
为什么 b.ResetTimer() 要放在子测试循环外
很多人误以为每个子测试都要调 b.ResetTimer(),其实只要在子测试函数体开头调一次就够了。它的作用是丢弃初始化开销(比如建 map、读配置)的耗时,只测核心逻辑。
常见错误是把它塞进 for 循环里,导致每次迭代都重置计时器,最终 b.N 失效,报告的 ns/op 严重偏低甚至为 0。
立即学习“go语言免费学习笔记(深入)”;
- 初始化代码(如
data := loadTestData())放b.ResetTimer()前 - 核心待测逻辑(如
fn(data))放b.ResetTimer()后 - 子测试函数内只需调一次
b.ResetTimer(),位置在for循环之前
表格驱动 benchmark 容易忽略的性能陷阱
表格驱动看着干净,但容易把不该共享的状态混进去,比如复用指针、全局缓存或未清空的 slice,导致后续子测试被前序影响。
典型表现是:单独跑某个子测试很快,合在一起跑却变慢;或者不同子测试的 ns/op 差异异常大,且顺序敏感。
- 每次子测试都该有独立的数据副本,避免
cases[i].data被多次修改 - 不要在表格里存指针或 map,除非你明确知道它们不会被修改
- 如果被测函数内部用了 sync.Pool 或缓存,记得在子测试间加
runtime.GC()(仅调试用,正式 benchmark 不要加) - 用
go test -bench=. -benchmem -count=3多跑几轮,看结果是否稳定——波动超过 5% 就得查干扰源
Go 1.21+ 的 SubBenchmarks 和旧写法兼容吗
Go 1.21 没新增任何 benchmark API,b.Run 仍是唯一方式。“SubBenchmarks”只是文档里对 b.Run 行为的统称,不是新函数。所有 Go 1.12+ 版本行为一致,无需适配。
但要注意:Go 1.20 开始默认启用 -cpu=1,而旧版默认用全部 CPU。如果你在 CI 里看到 benchmark 结果突变,先检查 go test 是否加了 -cpu 参数。
- 想复现老行为,显式加
-cpu=1,2,4;只关心单核性能就保持默认 - 子测试名里别含空格或斜杠,否则
go test -bench=xxx/yyy过滤会出错 - 子测试太多(>50 个)会导致
go test启动变慢,可按场景分文件,比如parse_bench_test.go和format_bench_test.go
最麻烦的其实是数据构造本身——它不计入计时,但若构造逻辑太重,会让 b.N 实际执行次数远低于预期。这时候得把构造提到 b.ResetTimer() 前,并确认它真没副作用。











