当前位置:首页 > 文章列表 > Golang > Go教程 > testing.B.Loop 如何重写旧式基准测试循环

testing.B.Loop 如何重写旧式基准测试循环

来源:17golang原创 2026-10-09 08:37:40 0浏览 收藏

把旧式 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删除
雨夜霓虹电车与湿润街道手机壁纸提示词雨夜霓虹电车与湿润街道手机壁纸提示词
上一篇
雨夜霓虹电车与湿润街道手机壁纸提示词
MySQL NOWAIT 锁定读取失败后如何设计快速降级
下一篇
MySQL NOWAIT 锁定读取失败后如何设计快速降级
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    386次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    468次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    475次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    415次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    241次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码