基准测试与性能回归
一句话结论:基准测试用 testing.B 量化某段代码的性能,b.N 由框架自动校准到稳定迭代次数;结果看三个指标:ns/op(耗时)、B/op(每次分配字节)、allocs/op(分配次数)。做基准测试要防止编译器“优化掉被测代码”(结果写进包级 sink 变量)并排除 setup 时间(b.ResetTimer)。性能回归靠 benchstat:把本次与基线两次 go test -bench 输出对比,由统计方法判定差异是否显著,接入 CI 后劣化超阈值即告警——微基准只看趋势,真实容量判断仍要配合压测(wrk/k6/ghz)。
基准测试骨架
var sink []byte // 包级变量,防止结果被编译器优化掉
func BenchmarkMarshalUser(b *testing.B) {
u := &User{Name: "张三", Age: 30}
b.ReportAllocs() // 输出 B/op 与 allocs/op
b.ResetTimer() // 排除上面的准备时间
for i := 0; i < b.N; i++ {
out, _ := json.Marshal(u)
sink = out // 必须消费结果
}
}
go test -bench=. -benchmem -count=5 -benchtime=1s | tee new.txt
# ns/op B/op allocs/op 三列都要看
回归对比(benchstat)
go install golang.org/x/perf/cmd/benchstat@latest
# 基线存 old.txt,改动后再跑存 new.txt
benchstat old.txt new.txt
# 输出 +/- 百分比与 p 值:只有统计显著的差异才算回归
- CI 实践:关键包维护基准测试 + 基线文件,MR 自动 benchstat,判“显著劣化”(如 +5%~10% 且 p<0.05)即失败。
- 比 ns/op 更稳的指标是 allocs/op:分配次数几乎不受机器噪声影响,先消分配再抠时钟周期。
- 微基准的坑:被测代码被内联/消除(用 sink)、准备时间混入计时(ResetTimer)、并行噪声(-count 多次取稳)、机器变频(同机对比,别跨机器比绝对值)。
适用对象与边界
- 值得基准化:JSON 序列化、校验、路由级中间件链、DAO 封装、核心算法。
- 不值得:IO/网络为主的操作(噪声大);handler 全链路对比请用压测工具打真实并发。
追问记忆点
- 追问:为什么循环里必须有 sink?——编译器发现结果没被使用可能整体删除被测调用,测出来全是 0。
- 追问:benchstat 解决什么问题?——两次跑都有噪声,它用统计检验判断“差异是真回归还是抖动”,比肉眼看数字可靠。
- 追问:bench 和压测什么关系?——bench 测单点函数、可回归;压测测整链路容量(QPS/延迟分位),二者都要。
- 记忆点:sink 防优化、ResetTimer 排除准备、allocs/op 看分配、benchstat 判回归、CI 门槛设阈值。