go test -bench 只能测同环境同配置下函数的相对性能倍数,不能测绝对耗时、内存抖动、GC干扰或跨版本吞吐量;合法Benchmark函数须以Benchmark开头、接收*testing.B参数、调用b.ResetTimer()重置计时、b.ReportAllocs()统计分配,且准备逻辑放Reset前、核心逻辑在b.N循环内。
go test -bench 能测什么,不能测什么
基准测试只反映函数在当前运行环境下的相对性能,不是绝对耗时。它不捕获内存分配抖动、gc 干扰或系统负载突变,也不适合对比跨版本 go 运行时的吞吐量。真正能信的,是同一台机器、相同 goos/goarch、关闭 cpu 频率调节(如 sudo cpupower frequency-set -g performance)下,两个 benchmark* 函数之间的倍数关系。
写一个合法的 Benchmark 函数必须满足哪些条件
Go 的 testing.B 机制会自动调用你的函数,并反复执行 b.N 次。若函数签名不对、没调用 b.ResetTimer() 或漏掉 b.ReportAllocs(),结果就不可靠。
- 函数名必须以
Benchmark开头,且接收唯一参数*testing.B - 必须在实际被测逻辑前调用
b.ResetTimer()(否则初始化开销计入耗时) - 若想看内存分配,需在
b.ResetTimer()后立即调用b.ReportAllocs() - 禁止在循环体外做一次性准备(如构建大 slice),应放在
b.ResetTimer()前;但也不能把准备逻辑塞进b.N循环里,否则测的是构造+执行
示例:
func BenchmarkMapAccess(b *testing.B) {
m := make(map[int]int)
for i := 0; i for i := 0; i <p>}</p>
如何避免常见的 benchmark 误判陷阱
最常踩的坑是编译器优化掉被测逻辑,导致结果恒为 0 ns/op —— 这不是快,是没跑。
- 所有计算结果必须“逃逸”出函数作用域:用
blackhole变量接住,或传给fmt.Print(仅调试)、或写入全局var result int(注意加b.StopTimer()再赋值) - 不要在
b.N循环里做随机数生成、时间获取等系统调用,它们本身开销大且不稳定 - 避免使用
range遍历 map 测查找性能,因为range是迭代顺序不确定的遍历,和单次m[key]完全不是一回事 - 如果被测函数返回 error,别忽略它:用
if err != nil { b.Fatal(err) },否则错误可能掩盖真实瓶颈
运行与解读 bench 输出的关键字段
执行 go test -bench=. -benchmem 后,输出形如:
BenchmarkMapAccess-8 10000000 124 ns/op 0 B/op 0 allocs/op
其中:
-
-8表示 GOMAXPROCS=8,即并发线程数;改用GOMAXPROCS=1 go test -bench=.可排除调度干扰 -
10000000是实际执行的轮数(b.N最终值),由 Go 自动调整到总耗时约 1 秒 -
124 ns/op是每次操作平均纳秒数,重点看它和对照组的比值 -
0 B/op和0 allocs/op来自-benchmem,表示每次操作零字节分配、零次堆分配 —— 若这两项突增,说明你触发了意外逃逸
如果看到 ns/op 值极小(比如 0.3),大概率是逻辑被优化掉了;如果 allocs/op 非零但你确信不该分配,用 go build -gcflags="-m -m" 检查逃逸分析。











