登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

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 基准在逐步扩大迭代数时,基准函数及其准备和清理可能被调用多次;计时器调用用来把非目标工作排除。迁移时不要把这三个区域压成一个循环,否则看起来代码更短,测到的东西却变了。

Go 旧式 b.N 基准与 testing.B.Loop 基准中准备区、测量区和清理区的静态结构对照图
图1:旧式 b.N 与 B.Loop 基准的准备、测量和清理边界静态对照图,不是运行截图。

用 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) // 只测原地排序
	}
}

如果业务问题本来就是“复制并排序一批数据的总成本”,那就不应该暂停计时。是否排除准备动作取决于基准问题,而不是模板。迁移的目标是保留原测量语义,不是让数字尽量小。

testing.B.Loop 中固定样本、工作缓冲区、循环内输入重置、计时控制和被测排序函数的静态依赖结构图
图2:可变输入基准中的数据所有权与计时边界静态说明图,不是实际基准运行结果。

自定义指标在循环结束后读取 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() 获取迭代。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>