当前位置:首页 > 文章列表 > Golang > Go问答 > testing.B.Loop 为什么不再需要手动读取 b.N

testing.B.Loop 为什么不再需要手动读取 b.N

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

testing.B.Loop 不再要求你手动读取 b.N,因为迭代次数的决定权已经从基准函数移回了 testing 框架。代码只要写成 for b.Loop() { ... }:返回 true 就执行一次待测操作,返回 false 就结束。框架在循环过程中自行扩展迭代目标,还会自动处理循环前后的计时边界。

不过,“不再需要手动读取”不等于 b.N 被废弃。B.Loop 返回 false 后,b.N 会保存最终执行次数,仍可用于计算 compares/op 这类自定义平均指标。

官方 API 文档:https://pkg.go.dev/testing#B.Loop

Go 官方设计解读:https://go.dev/blog/testing-b-loop

旧模型的瓶颈:基准函数必须服从外部给出的 b.N

我第一次把一批旧 Benchmark 改成 B.Loop 时,最容易误解的一点是:过去的 b.N 不是普通业务参数,而是测试框架与基准函数之间的控制协议。框架先给一个较小的 N,调用基准函数;如果测量时间不够,再放大 N 并重新调用。基准函数必须保证待测代码恰好执行 N 次。

func BenchmarkEncodeOld(b *testing.B) {
    // 准备固定输入;旧模型可能随着基准函数重入而重复执行这里。
    input := []byte("benchmark payload")

    // 排除循环前准备时间,避免把输入构造算入 ns/op。
    b.ResetTimer()
    for i := 0; i 

这个协议能工作,但规模一大,维护成本会明显上升:昂贵的初始化可能执行多次,漏写 ResetTimer 会把准备时间混进结果,未被使用的返回值还可能让编译器把待测调用消掉。问题不在 b.N 本身,而在于基准作者需要同时负责循环控制、计时边界和防优化细节。

b.N 风格基准中测试框架、基准函数和待测操作的静态边界结构图
图1:旧 b.N 模型的静态结构图。框架把迭代目标交给基准函数,函数内部负责读取 b.N、维护计时边界并调用待测操作;这不是运行截图。

新结构的关键:B.Loop 自己就是迭代控制接口

Go 1.24 加入 B.Loop 后,基准函数不再接收一个需要自己消耗完的循环上限,而是每轮向 Loop 询问“是否继续”。内部仍会做逐步扩容来摊薄测量开销,但这个过程不再暴露给调用者。对每次 measurement 而言,基准函数只调用一次,循环外的 setup 和 cleanup 也就只执行一次。

B.Loop 第一次被调用时会重置计时器,因此循环前的准备代码不计入测量;当它返回 false 时会停止计时器,因此循环后的清理与汇总也不计入测量。换句话说,新的结构把“什么时候开始测、什么时候结束测”绑定到了循环边界。

B.Loop 风格基准的调度边界、计时边界和报告边界静态结构图
图2:B.Loop 模型的静态结构图。迭代调度与计时边界由 B.Loop 管理,待测调用留在循环体内,最终 b.N 位于循环后的报告边界;这不是运行截图。

最小迁移:删除 b.N 循环,把条件改为 b.Loop()

绝大多数串行基准的迁移只需要两步:删除手动的 N 次循环,并把循环条件精确写为 b.Loop()。上面的编码基准可以改成:

func BenchmarkEncode(b *testing.B) {
    // 循环前只准备一次输入,首次调用 b.Loop 时会自动重置计时器。
    input := []byte("benchmark payload")

    for b.Loop() {
        // 只保留真正要测量的操作,不再读取或递减 b.N。
        hex.EncodeToString(input)
    }
}

这里没有把返回值赋给包级 sink。官方文档说明,条件精确写成 b.Loop() 时,编译器会让循环体内函数调用的参数、结果以及赋值变量保持存活,避免整个循环体被完全优化掉。这个保护只覆盖语法上位于花括号内的语句;把待测调用移到循环外,或把条件包装成别的辅助函数,都不能假定获得同样效果。

迁移后仍然可以使用原来的命令运行基准:

# 运行当前包中名称包含 Encode 的基准,并同时报告内存分配。
go test -run='^$' -bench='Encode' -benchmem

结果判断的重点不是追求某个固定数字,而是确认目标基准能够被发现、输出仍包含 ns/op,需要时也有 B/op 与 allocs/op。不同机器上的绝对值不应直接横向比较。

自动计时不是万能的:循环内准备仍要手动排除

B.Loop 自动排除的是整个循环之前和之后的工作。如果每轮都必须恢复输入,而你只想测核心操作,仍需要在循环内部使用 StopTimer 与 StartTimer。

func BenchmarkSort(b *testing.B) {
    // src 是稳定的原始数据,work 是每轮排序使用的副本。
    src := []int{9, 3, 7, 1, 5, 2}
    work := make([]int, len(src))

    for b.Loop() {
        // 复制只用于恢复输入,不属于要测量的排序操作。
        b.StopTimer()
        copy(work, src)
        b.StartTimer()

        // 每轮执行相同的待测工作,避免输入状态逐轮变化。
        slices.Sort(work)
    }
}

这也是新模型的一个明确代价:它减少了循环外计时管理,却不会替你判断循环内哪部分属于业务操作。文件读取、随机数据生成、连接重建等每轮准备工作是否应该计时,仍然需要基准作者根据问题边界决定。

b.N 仍有一个重要位置:循环结束后的指标汇总

B.Loop 结束后,最终迭代次数会写入 b.N。因此,自定义总量要换算成每次操作的平均值时,仍然可以读取它。区别在于:b.N 不再驱动循环,只负责事后报告。

func BenchmarkCompare(b *testing.B) {
    values := []int{5, 4, 3, 2, 1}
    var compares int64

    for b.Loop() {
        // 每轮复制一份数据,保证排序输入的初始状态一致。
        current := append([]int(nil), values...)
        slices.SortFunc(current, func(a, c int) int {
            // 记录比较总次数,循环结束后再换算为每次操作指标。
            compares++
            return cmp.Compare(a, c)
        })
    }

    // 此时 b.N 已是最终迭代数,可安全用于计算 compares/op。
    b.ReportMetric(float64(compares)/float64(b.N), "compares/op")
}

如果你的代码在循环体内读取 b.N 来决定分支、分片大小或当前进度,迁移时不能原样照搬。B.Loop 的设计目标之一就是不让单次操作依赖总迭代数或当前迭代位置。应把每轮输入固定下来,或使用与测量次数无关的数据源。

迁移时要保留的限制

场景建议原因
同一个 Benchmark只保留一个 for b.Loop()不要同时混用 b.N 风格循环
每轮工作保持语义一致不同轮次做不同事情会污染平均值
并行基准继续使用 b.RunParallel 与 pb.Next()B.Loop 面向普通串行基准循环
工具链版本确认 Go 1.24+B.Loop 从 Go 1.24 加入
自定义指标在循环结束后读取 b.N此时它保存最终迭代次数

对我来说,B.Loop 最有价值的地方不是少写了一个变量,而是把容易出错的控制职责收回到框架内部。代价则是迁移时必须重新检查计时边界,尤其是循环内准备、状态恢复和并行基准,不能机械替换语法后就结束。

常见问题

B.Loop 会让基准函数执行多少次?

每次 measurement 中,基准函数只执行一次,B.Loop 在这一次调用内控制循环持续多久。使用 -count 时,每个 count 仍会有各自的 measurement。

用了 B.Loop 还需要 ResetTimer 吗?

循环前的 setup 通常不需要,因为第一次调用 B.Loop 会自动重置计时器。循环内不想测量的准备工作仍可能需要 StopTimer 和 StartTimer。

用了 B.Loop 还需要全局 sink 防止优化吗?

通常不需要。条件精确写为 b.Loop() 时,编译器会保护循环体内函数调用的参数和结果不被完全消除。但保护有明确的语法范围,不能推广到循环外或自行封装后的条件。

B.Loop 可以和 b.RunParallel 一起替换 pb.Next 吗?

不能直接这样替换。并行基准仍由 RunParallel 创建 goroutine,并通过各 goroutine 中的 pb.Next() 分配总迭代次数。

所以,标题问题的准确答案是:B.Loop 让框架自己掌握“还要跑几次”,基准函数不再用 b.N 驱动循环;但在循环结束后,b.N 仍是最终次数的报告接口。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Postman 如何用变量范围隔离测试与生产环境Postman 如何用变量范围隔离测试与生产环境
上一篇
Postman 如何用变量范围隔离测试与生产环境
多模态模型输入图片过大时如何控制视觉令牌
下一篇
多模态模型输入图片过大时如何控制视觉令牌
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之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次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码