合法benchmark函数须满足:文件名以_test.go结尾、函数名以benchmark开头、参数为*testing.b、循环使用b.n;否则go test -bench会静默忽略;必须用b.resettimer()分离初始化与计时,用b.reportallocs()测内存,强制使用返回值防编译器优化。

Go 基准测试不是写个 func BenchmarkXxx 就能信的——不控制计时边界、不防编译器优化、不看内存分配,跑出来的 ns/op 数字基本是噪声。
为什么 go test -bench=. 什么都没输出或显示 0.00 ns/op
最常见原因是函数没满足硬性准入条件:文件名不是 *_test.go、函数名没以大写 Benchmark 开头、参数不是 *testing.B,或者循环体里没用 b.N。
- 写成
func BenchmarkFoo(t *testing.T)→ 被完全忽略,不报错也不提示 - 放在普通
.go文件里 →go test -bench=.扫不到,输出no benchmarks to run - 循环写成
for i := 0; i (固定次数)→ <code>b.N不生效,框架无法校准耗时,常导致0.00 ns/op或极低值
怎么写一个不被编译器优化掉的 Benchmark 函数
Go 编译器发现你算完结果没用,会直接把整个循环删成空操作。典型表现是 b.N 高达千万甚至上亿次,但 ns/op 低到反常识(比如 0.32 ns/op)。
- 必须让结果“逃不出去”:用包级变量接收,如
var blackhole int,然后在循环末尾写blackhole = compute() - Go 1.21+ 可改用
b.ReportMetric(0, "op"),但更稳妥的是做无害副作用,例如_ = result & 1 - 绝对不要在循环里调
fmt.Println、log.Print或time.Now()—— 它们强制保留计算,但引入巨大 I/O 噪声 - 验证是否真被优化:临时注释掉赋值语句,如果
ns/op骤降到 0,说明之前确实被删了
go test -bench 输出里的 ns/op 和 B/op 到底怎么看
ns/op 是每次操作平均纳秒数,数值越小越好;B/op 是每次操作平均分配字节数,反映堆压力。两者不总是一致——比如 CPU 优化了,但用了更多临时变量,ns/op 下降而 B/op 上升。
-
B/op是高频调用场景的关键瓶颈:哪怕单次只多分配 16 字节,每秒百万次调用就是 16MB/s 堆分配,GC 频率会明显升高 - 不加
-benchmem参数,默认不显示B/op和allocs/op,即使函数实际在疯狂分配 -
B/op显示为 0 不代表没分配,可能是逃逸分析判定变量未逃逸(栈上分配),也可能是忘了调b.ReportAllocs() - 必须在
for循环前调b.ReportAllocs(),写在循环里或之后等于没开
如何对比两个实现的真实性能差异
单独跑 BenchmarkA 和 BenchmarkB 然后比数字?完全不可靠。两次运行受 GC 周期、CPU 频率抖动、后台进程干扰完全不同,尤其当分配行为差异大时,ns/op 波动剧烈。
- 必须用
b.Run()在同一个基准生命周期内执行:共享 GC 周期、预热状态、调度上下文 - 命令要加
-count=5多轮采样,并用benchstat做统计显著性检验,不是简单取平均 - 推荐命令组合:
go test -bench=^BenchmarkCompare$ -benchmem -benchtime=5s -count=5 -run=none - 如果初始化开销大(比如建 map、读配置),一定要放
b.ResetTimer()前,否则 setup 时间会被平摊进ns/op
真正难的不是写出来,而是让结果不被编译器骗、不被 GC 干扰、不被系统噪声淹没——这些细节漏掉任意一环,基准测试就从工具变成幻觉。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











