正确运行 go test -bench 需固定 gomaxprocs=1、用 -benchmem/-count=5/-benchtime=5s、避免 i/o 和全局变量、前置准备放 b.resettimer() 前、用 benchstat 分析结果并关注 gc 开销与数据特征。

如何用 go test -bench 正确跑出可比对的基准数据
直接跑 go test -bench=. 很容易得出错误结论——默认不固定迭代次数、不禁止 GC 干扰、不控制 CPU 亲和性。真实对比必须显式约束执行环境。
- 加
-benchmem同时观察内存分配,避免只看耗时忽略 alloc 次数激增 - 用
-count=5 -benchtime=5s多轮采样并延长单轮时长,降低调度抖动影响 - 关键:加上
GOMAXPROCS=1环境变量,排除 goroutine 调度开销干扰纯算法差异 - 若测试涉及系统调用(如文件/网络),需提前 warm up 或隔离环境,否则
BenchmarkXXX-8中的-8(表示 8 个 P)会引入不可控变量
Benchmark 函数里为什么不能用 fmt.Println 或全局变量
任何非被测逻辑的 I/O、锁、内存分配都会污染结果。Go 基准框架在循环中反复调用你的函数,b.N 是框架动态调整的迭代次数,目的是让单次运行时间落在合理区间(通常 100ms–1s)。一旦你写死循环次数或混入副作用,b.N 就失效了。
- 禁止在
BenchmarkXXX函数体里调用fmt.Println、log.Print—— 它们触发 syscall 和内存分配,耗时波动远超算法本身 - 避免读写包级变量,尤其是 map、slice、mutex;并发运行时可能触发竞争,且无法反映单次调用开销
- 正确做法:所有前置准备(如构造输入数据)放在
b.ResetTimer()之前,用b.StopTimer()/b.StartTimer()控制计时区间 - 示例:
func BenchmarkParseInt(b *testing.B) { data := make([]string, b.N) for i := range data { data[i] = "12345" } b.ResetTimer() // 从这开始计时 for i := 0; i < b.N; i++ { _ = strconv.Atoi(data[i]) } }
对比两个实现时,benchstat 比手动看数字靠谱得多
人眼判断 “12.3ns vs 11.9ns” 是否显著快了 3% 是危险的。Go 官方工具链自带 benchstat,它用 Welch’s t-test 分析多轮采样分布,输出带置信区间的相对差异。
- 先分别保存两组结果:
go test -bench=BenchFoo -count=10 -benchmem > old.txt,改代码后再跑> new.txt - 执行
benchstat old.txt new.txt,输出类似geomean 1.04x faster或p=0.002 (significant) - 注意:如果某组标准差 > 5%,说明环境不稳定(如后台进程抢占 CPU),需重测,
benchstat不会帮你过滤噪声 - 不推荐用
benchcmp(已废弃),它只做简单均值比,无统计检验
GC 对比结果的影响常被低估,尤其在切片/字符串操作场景
短生命周期对象(如 strings.Split 返回的 slice)在高频调用下会持续触发 minor GC,而预分配缓冲区(如 bytes.Buffer 复用)能压低 GC 频率。但 benchmem 显示的 allocs/op 只是表象,真正要观察的是 GC pause 时间占比。
- 加
-gcflags="-m"确认编译器是否逃逸分析失败,导致本该栈上的变量堆分配 - 用
runtime.ReadMemStats在Benchmark前后采集PauseTotalNs,计算 GC 开销占比(需关闭GOGC避免干扰) - 典型陷阱:用
fmt.Sprintf拼接字符串 vsstrings.Builder—— 前者每轮产生 2–3 次 alloc,后者复用底层 slice,allocs/op 相差 10 倍以上 - 记住:低 allocs/op 不一定等于低延迟,但高 allocs/op 几乎一定拖慢吞吐,尤其在 p99 场景
strconv 和用混合 Unicode 的用户昵称测 strings.Contains,结论完全不可迁移。务必按线上真实分布构造输入样本。











