testing.B.Loop 如何重写旧式基准测试循环
来源:17golang原创
时间:2026-10-09 08:37:40 315浏览 收藏
把旧式 Go 基准测试迁移到 testing.B.Loop,最小改法是把 for i := 0; i 换成 for b.Loop(),并把一次性准备和清理保留在循环外。循环第一次调用 b.Loop() 时会自动重置计时器,返回 false 时会停止计时,因此多数基准可以删除循环前的 b.ResetTimer() 和循环后的 b.StopTimer()。但每轮都要重建输入的数据准备仍需留在循环内,并继续用 StopTimer/StartTimer 排除。
官方文档:https://pkg.go.dev/testing#B.Loop
B.Loop 不是机械换一个循环条件。迁移时要同时确认三件事:被测工作是否完全位于循环体内、一次性准备是否移出计时区、每轮输入重置是否仍按原意计时。只有测量边界相同,新旧结果才值得比较。
先保存旧基准的可比基线
我做这类迁移时不会先批量替换代码,而是先固定环境并保存旧结果。原因很简单:B.Loop 会自动排除循环外的准备与清理,如果旧基准忘了调用 ResetTimer,迁移后 ns/op 变小可能只是测量边界被修正,并不代表生产代码变快。
先在同一台机器、同一 Go 工具链、相同 CPU 设置和相同代码提交上执行多轮:
# 只运行目标基准,排除普通测试,并记录分配指标与 10 轮样本 go test -run='^$' -bench='^BenchmarkEncodeOrder$' -benchmem -count=10 ./... > old.txt
基线至少保留 ns/op、B/op、allocs/op 和运行环境。不要把单次输出当成结论;频率调节、后台负载、温度和垃圾回收都可能带来噪声。
识别旧循环里哪些代码真正属于被测工作
先看一个典型的旧式基准:
func BenchmarkEncodeOrderOld(b *testing.B) {
order := sampleOrder() // 一次性构造稳定输入,不属于编码耗时
b.ReportAllocs()
b.ResetTimer()
for i := 0; i
这里有三个区域:循环外的输入准备、循环内的被测调用、循环后的清理。旧式 b.N 基准在逐步扩大迭代数时,基准函数及其准备和清理可能被调用多次;计时器调用用来把非目标工作排除。迁移时不要把这三个区域压成一个循环,否则看起来代码更短,测到的东西却变了。

用 B.Loop 完成最小重写
对应的新写法如下:
func BenchmarkEncodeOrder(b *testing.B) {
order := sampleOrder() // 第一次调用 b.Loop 前完成,不计入测量
b.ReportAllocs()
for b.Loop() {
encodeOrder(order) // 循环体只保留每次都要测量的工作
}
releaseOrder(order) // b.Loop 返回 false 后计时器已停止
}
Go 1.24 引入了 B.Loop。它让每次 measurement 中的基准函数只执行一次,内部自行调整迭代目标;第一次调用会重置计时器,结束时会停止计时。编译器还会对条件精确写成 b.Loop() 的循环做特殊处理,让循环体里的函数参数、返回值和赋值变量保持存活,降低被测调用被完全消除的风险。
“精确写成”很重要。不要为了抽象把它包成 for shouldContinue(b),也不要在条件里再拼其他表达式。官方文档明确说,这项编译器处理只适用于花括号之间的语句,循环条件必须正好是 b.Loop()。
每轮都要重建输入时保留局部计时控制
并不是迁移后所有 StopTimer 都能删掉。原地排序、解码到复用缓冲区、消费队列等操作会修改输入,每轮都需要恢复相同初始状态。输入重置必须在循环内,却不一定属于被测成本。
func BenchmarkSortInts(b *testing.B) {
seed := makeSeed(4096) // 固定原始样本,只准备一次
work := make([]int, len(seed))
b.ReportAllocs()
for b.Loop() {
b.StopTimer()
copy(work, seed) // 每轮恢复输入,但不把复制时间算进排序
b.StartTimer()
slices.Sort(work) // 只测原地排序
}
}
如果业务问题本来就是“复制并排序一批数据的总成本”,那就不应该暂停计时。是否排除准备动作取决于基准问题,而不是模板。迁移的目标是保留原测量语义,不是让数字尽量小。

自定义指标在循环结束后读取 b.N
B.Loop 运行期间不要依赖最终迭代总数。它返回 false 后,b.N 才包含实际完成的迭代次数,可以用于计算每次操作的平均指标:
func BenchmarkParseBatch(b *testing.B) {
input := makeBatch()
var parsed int64
for b.Loop() {
parsed += int64(parseBatch(input)) // 累计循环实际处理的项目数
}
// 循环结束后 b.N 已确定,可安全计算每次操作的平均项目数。
b.ReportMetric(float64(parsed)/float64(b.N), "items/op")
}
同一个基准函数应当选择 B.Loop 或显式 b.N 循环,不能混用。每轮循环也应完成相同工作,不能根据尚未结束的迭代计数改变负载。子基准可以各自在自己的回调里使用 B.Loop;并行基准仍使用 b.RunParallel 与 pb.Next(),不要把并行迭代器机械替换成 b.Loop()。
用相同命令收集新结果
重写后使用与基线完全相同的筛选和轮数:
# 使用与旧版本一致的参数采集新结果,避免比较条件漂移 go test -run='^$' -bench='^BenchmarkEncodeOrder$' -benchmem -count=10 ./... > new.txt # benchstat 来自官方 golang.org/x/perf,用统计结果比较两组样本 benchstat old.txt new.txt
观察结果时分两层判断:第一层是基准边界是否一致,比如旧代码是否把准备时间算进去了;第二层才是指标是否显著变化。若只是从手动计时迁移到自动计时,在旧基准本就正确的情况下,核心操作的 ns/op 和分配指标应该保持同一量级。若差异很大,优先检查输入是否移出循环、返回值是否被使用、每轮状态是否相同,而不是立即宣布性能提升。
迁移时最容易踩的五个坑
| 改法 | 问题 | 正确处理 |
|---|---|---|
保留 b.N 循环,再在内部调用 b.Loop() | 混用两套迭代控制 | 每个基准只保留一种循环 |
把 b.Loop() 包进辅助函数 | 失去精确语法对应的编译器保护 | 条件直接写成 for b.Loop() |
| 把每轮输入重置移到循环外 | 后续迭代处理的是已修改数据 | 重置仍放循环内,按目标决定是否暂停计时 |
在循环内读取最终 b.N | 迭代总数尚未提交 | 循环结束后再用于自定义平均指标 |
| 看到 ns/op 下降就认定代码变快 | 可能只是准备与清理被排除 | 先核对测量边界,再比较多轮样本 |
适合怎样推进迁移
对我来说,最稳妥的顺序是按包逐个迁移,而不是全仓库搜索替换:先保存旧结果,再改一个基准,确认工作量和计时边界,最后比较多轮指标。纯函数、一次性输入、无循环内状态的基准通常可以直接改;涉及原地修改、网络、磁盘、随机数据、并行执行或自定义指标的基准要单独审视。
如果项目仍需兼容 Go 1.23 或更早版本,就不能直接使用 B.Loop。应先提升 go.mod、本地工具链和 CI 的最低 Go 版本,或暂时保留旧式 b.N 写法。
常见问题
用了 B.Loop 后还要调用 ResetTimer 吗?
循环外的一次性准备通常不需要;第一次调用 b.Loop() 会自动重置计时器。循环内需要排除的每轮准备仍使用 StopTimer 和 StartTimer。
B.Loop 会自动防止所有编译器优化吗?
不会。它会让精确 for b.Loop() 循环体中的函数参数、结果和赋值变量保持存活,防止循环体被完全消除,但不能保证每一种局部优化都被关闭。基准仍要表达真实工作负载。
迁移后还能在循环外使用 b.N 吗?
可以。B.Loop 返回 false 后,b.N 保存实际迭代次数,适合计算 items/op 等自定义平均指标。
RunParallel 也改成 B.Loop 吗?
不改。并行基准继续由 b.RunParallel 创建工作协程,并在每个协程中使用 pb.Next() 获取迭代。
-
483 收藏
-
118 收藏
-
242 收藏
-
444 收藏
-
117 收藏
-
316 收藏
-
Golang · Go教程 | 2小时前 | web安全 · Go教程 · net/http · CSRF防护 Go CrossOriginProtection AddTrustedOrigin 可信子域 跨源写请求467 收藏
-
245 收藏
-
104 收藏
-
441 收藏
-
200 收藏
-
178 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习