Go 1.24 新方法:编写性能测试用例方法 testing.B.Loop 介绍

Go 开发者在使用 testing包编写基准测试用例时,如果不注意,可能会遇到各种陷阱。这些陷阱,导致基准测试结果不准确。Go1.24 版本引入了一种新的基准测试编写方式,它同样易用,并且可以帮助规避编写基准测试时的一些坑。

Go 1.24 版本推荐使用 testing.B.Loop代替 testing.B.N来编写基准测试用例。

Go1.24 版本前,我们使用 b.N 来编写基准测试用例,例如:

go 复制代码
func Benchmark(b *testing.B) {
  for range b.N {
    ... 要测量的代码 ...
  }
}

改用b.Loop仅需要微不足道的改动:

go 复制代码
func Benchmark(b *testing.B) {
  for b.Loop() {
    ... 要测量的代码 ...
  }
}

testing.B.Loop有很多优点:

  • 可以防止基准测试循环内的不当编译优化;
  • 可以自动将设置和清理部分代码耗时从基准测试时间统计中剔除;
  • 代码不应意外地依赖于总迭代次数或当前迭代。

上述这些优点都是在使用 b.N编写基准测试代码时易犯的错误,这些错误会导致基准测试不准确。除了上述优点之外,b.Loop风格的基准测试,还能再更短的时间执行完。

接下来,我们来看下testing.B.Loop的优势以及如何有效地使用它。

旧基准测试循环问题

在 Go 1.24 之前,大部分的基准测试用例结构简单。但是复杂的测试用例,却需要很小心的编写:

go 复制代码
func Benchmark(b *testing.B) {
  ... setup ...
  b.ResetTimer() // 如果设置可能很昂贵
  for range b.N {
    ... 代码测量 ...
    ... 使用汇点或累积防止未用代码消除 ...
  }
  b.StopTimer() // 如果清理或报告可能很昂贵
  ... 清理 ...
  ... 报告 ...
}

如果设置 (setup)或清理 (cleanup)逻辑复杂,耗时较久,为了避免这些准备性的逻辑参与到核心代码的耗时统计中,需要使用ResetTimerStopTimer 方法,将这些时间剔除掉。但是真正开发的过程中, 可能有一些开发者会遗漏这些逻辑。即使开发者没有遗漏,也很难判断设置或清理过程是否"足够耗时"到需要使用它们。

还有一个更微妙的陷阱,需要更深入的理解(示例源代码):

go 复制代码
func isCond(b byte) bool {
  if b%3 == 1 && b%7 == 2 && b%17 == 11 && b%31 == 9 {
    return true
  }
  return false
}

func BenchmarkIsCondWrong(b *testing.B) {
  for range b.N {
    isCond(201)
  }
}

在这个例子中,用户可能会观察到 isCond 在亚纳秒级别的时间内执行。CPU 的速度很快,但并没有快到这个程度!这个看似异常的结果源于 isCond 被内联处理,并且由于其结果从未被使用,编译器将其视为无效代码而进行消除。

因此,这个基准测试根本没有测量 isCond。它测量的是进行无操作所需的时间。在这种情况下,亚纳秒的结果是一个明显的警示,但在更复杂的基准测试中,部分无效代码消除可能导致看起来合理但实际上并未测量预期内容的结果。

testing.B.Loop可以带来哪些好处?

b.N 风格的基准测试不同,testing.B.Loop 能够跟踪其首次调用时间以及基准测试的最终迭代结束时刻。循环开始时的 b.ResetTimer 和结束时的 b.StopTimer 被整合进 testing.B.Loop,消除了手动管理基准测试计时器以进行初始化和清理代码的开发步骤。

另外,Go 编译器现在可以探测到只调用 testing.B.Loop 时的循环,并阻止 testing.B.Loop 内的代码死循环。

testing.B.Loop 的另一个优点是其一次性快速提升的方法。对于 b.N 风格的基准测试,测试包必须多次调用基准测试函数,并使用不同的 b.N 值逐步增加,直到测量时间达到一个阈值。相比之下,b.Loop 可以简单地运行基准测试循环,直到达到时间阈值,只需调用基准测试函数一次。

b.N 风格循环的某些限制仍适用于 b.Loop 风格的循环。用户仍需在必要时负责在基准测试循环中管理计时器。(示例源

go 复制代码
func BenchmarkSortInts(b *testing.B) {
  ints := make([]int, N)
  for b.Loop() {
    b.StopTimer()
    fillRandomInts(ints)
    b.StartTimer()
    slices.Sort(ints)
  }
}

在这个例子中,为了测试 slices.Sort 方法的就地排序性能,每次迭代都需要一个随机初始化的数组。开发菏泽仍需在这些情况下手动管理计时器。

此外,基准测试函数体中仍需要只有一个这样的循环(b.N风格循环不能与b.Loop风格循环共存),并且循环的每次迭代应该做相同的事情。

何时使用

testing.B.Loop 方法,是从 Go 1.24 版本起,编写基准测试的首选方式。使用示例如下:

go 复制代码
func Benchmark(b *testing.B) {
  ... 设置 ...
  for b.Loop() {
    // 可选的循环内部设置/清理计时器控制
    ... 要测量的代码 ...
  }
  ... 清理 ...
}
  • 知识星球:令飞编程。10+ 高质量体系课( Go、云原生、AI Infra)、15+ 高质量实战项目,P8 技术专家助你提高技术天花板,冲击百万年薪!
  • 我公众号:令飞编程,分享 Go、云原生、AI Infra 相关技术。回复「资料」免费下载 Go、云原生、AI 等学习资料;
  • 哔哩哔哩:令飞编程 ,以视频、直播的形式,分享技术、职场、课程、面经等;
相关推荐
清同趣科研2 分钟前
R绘图|6种NMDS(非度量多维分析)绘图保姆级模板——NMDS从原理到绘图,看师兄这篇教程就够了
人工智能·算法
凡人的AI工具箱8 分钟前
PyTorch深度学习框架60天进阶学习计划 - 第41天:生成对抗网络进阶(三)
人工智能·pytorch·python·深度学习·学习·生成对抗网络
workworkwork勤劳又勇敢9 分钟前
Adversarial Attack对抗攻击--李宏毅机器学习笔记
人工智能·笔记·深度学习·机器学习
java1234_小锋29 分钟前
Zookeeper的典型应用场景?
分布式·zookeeper·云原生
乌旭42 分钟前
从Ampere到Hopper:GPU架构演进对AI模型训练的颠覆性影响
人工智能·pytorch·分布式·深度学习·机器学习·ai·gpu算力
声网1 小时前
从开发者视角解读 Google Cloud Next 25
人工智能
DragonnAi2 小时前
基于项目管理的轻量级目标检测自动标注系统【基于 YOLOV8】
人工智能·yolo·目标检测
AI绘画咪酱2 小时前
【CSDN首发】Stable Diffusion从零到精通学习路线分享
人工智能·学习·macos·ai作画·stable diffusion·aigc
DeepSeek+NAS2 小时前
耘想WinNAS:以聊天交互重构NAS生态,开启AI时代的存储革命
人工智能·重构·nas·winnas·安卓nas·windows nas
2201_754918412 小时前
OpenCv--换脸
人工智能·opencv·计算机视觉