基准测试的迭代次数由 b.N 动态控制,Go 运行时根据 -benchtime(默认1秒)自动调整 b.N 以获得稳定平均耗时,禁止手动设定或硬编码循环次数。

基准测试的迭代次数由 b.N 控制,但你不能手动设值
Go 的基准测试不接受“运行 1000 次”这种硬编码逻辑。你写的循环必须用 b.N,而这个值由 Go 运行时动态决定——它会反复试探、调整 b.N,直到单次基准函数执行总时长接近默认的 -benchtime=1s(约 1 秒),从而获得统计上稳定的平均耗时。
常见错误现象:
- 手写
for i := 0; i → 结果不可比,ns/op失真,且无法反映真实吞吐能力 - 在循环外做耗时初始化(如构建大 map、读文件),却没调用
b.ResetTimer()→ 初始化时间被计入性能指标 - 误以为
b.N是“用户指定次数”,试图打印或修改它 →b.N是只读的,修改无效,还可能触发 panic
正确做法:
- 把真正要测的逻辑放在
for i := 0; i 循环体内 - 初始化代码放循环外;若耗时明显,紧接其后加
b.ResetTimer() - 需要更长/更短测试时间?用命令行参数控制:
go test -bench=. -benchtime=5s或-benchtime=10000x(后者强制执行 10000 次,不自适应)
func BenchmarkJSONMarshal(b *testing.B) {
data := map[string]int{"a": 1, "b": 2}
b.ResetTimer() // 忽略上面构造 map 的时间
for i := 0; i < b.N; i++ {
_ = json.Marshal(data)
}
}
go test -bench 命令背后的实际执行流程
运行 go test -bench=. 时,Go 并不是直接跑一遍就出结果。它先以极小的 b.N(比如 1 或 10)试跑一次,估算单次耗时;再根据目标时间(默认 1 秒)反推一个合理的 b.N,重新执行;若仍偏差较大,还会再调优——整个过程可能重试 2–4 轮。
这导致几个关键事实:
- 输出里的
1000000(迭代次数)是最终稳定后的b.N,不是你写的数字 - 同一台机器、同一段代码,两次
go test -bench=.的b.N可能略有不同(因系统负载、GC 时间波动) - 若函数极快(如纳秒级),
b.N会很大;若极慢(如毫秒级),b.N可能只有个位数 -
BenchmarkFoo-8中的-8表示当时GOMAXPROCS=8,对并发测试有意义,普通基准可忽略
实用技巧:
- 想固定次数做对比?用
-benchtime=10000x,避免自适应干扰横向比较 - 怀疑 GC 影响结果?加
-gcflags="-l"关闭内联(谨慎),或用runtime.GC()在b.ResetTimer()后手动触发一次 - 观察是否收敛?加
-count=3运行三次,看ns/op波动是否小于 2%
为什么不能在循环里做变量捕获或副作用操作
基准测试的目标是测量「纯计算路径」的开销。如果在for 循环里创建新对象、拼接字符串、调用 fmt.Println,这些行为会污染内存分配和 CPU 时间,让 ns/op 和 B/op 失去参考价值。
典型陷阱:
-
result := someFunc(); fmt.Sprintf("%v", result)→fmt分配内存,掩盖了someFunc本身性能 -
s := "hello" + strconv.Itoa(i)→ 每次迭代都新建字符串,B/op飙升,但和你要测的逻辑无关 - 把
var buf bytes.Buffer放在循环内 → 每次都 new 一个,分配放大 100 倍
正确姿势:
- 只测核心逻辑,其他全剥离到循环外或用
_ =抑制返回值 - 需复用对象?定义在循环外,循环内重置(如
buf.Reset()) - 不确定是否有隐式分配?加
-benchmem看B/op和allocs/op,非零就要警惕
并行基准测试中 b.RunParallel 的特殊机制
b.RunParallel 不是简单地开 goroutine 跑 b.N 次,而是把总工作量(大致)均分给 P 个 worker(P 默认为 GOMAXPROCS),每个 worker 自己维护一个本地计数器,通过 pb.Next() 按需取任务,直到全局完成 b.N 次。
这意味着:
- 你不能在
pb.Next()外访问b.N来控制循环——worker 不知道总次数,只认pb.Next() - 初始化(如建连接池、预热缓存)必须放在
b.RunParallel外,否则每个 goroutine 都重复执行 - 若被测逻辑有共享状态(如全局 map),必须自己加锁或用
sync.Map,否则结果不可信 - 想限制并发度?用
b.SetParallelism(2),而非改GOMAXPROCS
func BenchmarkSyncMapStore(b *testing.B) {
m := new(sync.Map)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
m.Store("key", "value")
}
})
}
真正难的不是写对语法,而是判断哪部分该计入、哪部分该剔除,以及如何让 b.N 的自适应过程不掩盖你要验证的性能变化。每次改完代码跑 go test -bench=. -benchmem -count=3,盯着三行 ns/op 是否稳定,比任何理论都管用。










