benchstat 是专用于分析 Go 基准测试结果的统计工具,它不运行基准测试,仅处理 go test -bench 生成的多轮(需 -count=N)输出文件(如 old.txt/new.txt),通过对比均值、中位数、p 值判断性能变化是否显著;不支持单次终端输出或非标准格式,要求两次测试环境、命名、字段一致,核心用途是跨提交/配置的回归分析。

benchstat 是什么,它不做什么
benchstat 不是运行基准测试的工具,它只负责分析 go test -bench 生成的原始结果。你必须先用 Go 自带的测试框架跑出多轮 Benchmark* 输出(通常是 JSON 或文本格式),benchstat 才能对比均值、中位数、p 值,判断性能变化是否显著。
常见错误现象:直接对单次 go test -bench=. 的终端输出喂给 benchstat,结果报错 no benchmarks found —— 因为默认输出不含足够统计信息,也没保存成文件。
- 必须加
-count=N(比如-count=10)跑多轮,否则 benchstat 没法算波动 - 输出要重定向到文件,例如:
go test -bench=. -count=10 -benchmem > old.txt - benchstat 默认只认标准格式;如果用了
-json,得配benchstat -json
怎么比两次基准测试才靠谱
benchstat 的核心价值在于「跨提交 / 跨配置」比较,不是看单次数字。它会自动对齐同名 benchmark,忽略无关行,但前提是两组数据结构一致、命名一致、字段完整。
使用场景:PR 前后性能回归检查、GC 参数调优验证、函数内联与否的影响评估。
立即学习“go语言免费学习笔记(深入)”;
- 确保两次测试用的 Go 版本、构建参数(如
-gcflags)、运行环境(CPU 频率、后台负载)尽量一致,否则差异可能被误判为代码问题 - 文件名不重要,但 benchmark 名必须完全匹配,
BenchmarkFoo和BenchmarkFooParallel被视为不同项 - 推荐用
benchstat old.txt new.txt,它默认按几何平均比值排序,把变化最大的放前面 - 若想排除噪音,可加
-delta-test=none关掉统计检验,只看数值差;或加-geomean强制用几何均值
输出里哪些数字真该盯住
benchstat 默认输出三列:基准值(old)、新值(new)、变化率(delta)。但真正影响判断的是最后一列的 p 值和 Δ 符号,不是百分比本身。
性能 / 兼容性影响:Go 1.21+ 默认开启 GOEXPERIMENT=fieldtrack,某些内存分配指标可能漂移;benchstat 不感知这些底层变化,全靠你人工过滤。
-
123ns ± 5%中的 ± 是标准差比例,不是误差范围;小于 ±2% 通常认为稳定,大于 ±8% 就该怀疑环境干扰 - 看到
1.02x(即 +2%)但p=0.003,说明涨得小但极大概率真实 —— 别急着归因为“没变” - 出现
NaN或inf:通常是某轮耗时为 0 或溢出,删掉那行再跑,别留着参与统计 - 注意
Allocs/op和B/op的单位,它们不随 CPU 变化,比ns/op更适合跨机器比
容易被忽略的启动成本和 warmup 问题
benchstat 不处理预热(warmup),而 Go 的 benchmark 在前几轮常有明显抖动 —— 特别是涉及 map、sync.Pool、反射缓存的场景。它直接拿全部 -count 轮次算均值,结果可能被头两轮拖低。
这不是 bug,是设计使然:Go 认为 benchmark 应该自己 handle warmup,而不是靠工具切数据。
- 手动跳过前 N 轮:用
benchstat -alpha=0.05 -geomean old.txt new.txt没用,得在生成数据时控制,比如写个 wrapper 脚本丢掉前 2 行 - 更稳妥的做法:在
Benchmark函数里加b.ReportMetric(0, "warmup")并忽略该 metric,但这需要 Go 1.22+ - 如果你发现
ns/op波动始终 >10%,先go tool trace看 GC 是否在 benchmark 过程中触发,benchstat 完全看不到这个层面
benchstat 的边界很清晰:它只信数字,不信上下文。环境噪声、编译器优化路径切换、甚至 CPU 微码更新,都可能让同一份代码在不同时间跑出不可比的结果 —— 这时候别怪工具,得回到 go test -bench 的原始输出里翻每一轮的具体值。











