Go 博客
使用 testing.B.Loop 进行更可预测的基准测试
使用 testing
包编写基准测试的 Go 开发者可能遇到过它的各种陷阱。Go 1.24 引入了一种新的编写基准测试的方法,它同样易于使用,但同时更健壮:testing.B.Loop
。
传统上,Go 基准测试是使用从 0 到 b.N
的循环编写的
func Benchmark(b *testing.B) {
for range b.N {
... code to measure ...
}
}
改为使用 b.Loop
是一个微不足道的改变
func Benchmark(b *testing.B) {
for b.Loop() {
... code to measure ...
}
}
testing.B.Loop
有许多优点
- 它防止了基准测试循环中不必要的编译器优化。
- 它自动将设置和清理代码排除在基准测试计时之外。
- 代码不会意外地依赖于总迭代次数或当前迭代。
这些都是使用 b.N
风格基准测试时容易犯的错误,它们会悄无声息地导致虚假的基准测试结果。此外,b.Loop
风格的基准测试甚至完成得更快!
让我们探讨一下 testing.B.Loop
的优势以及如何有效地利用它。
旧基准测试循环的问题
在 Go 1.24 之前,虽然基准测试的基本结构很简单,但更复杂的基准测试需要更多的注意
func Benchmark(b *testing.B) {
... setup ...
b.ResetTimer() // if setup may be expensive
for range b.N {
... code to measure ...
... use sinks or accumulation to prevent dead-code elimination ...
}
b.StopTimer() // if cleanup or reporting may be expensive
... cleanup ...
... report ...
}
如果设置或清理并非微不足道,开发者需要用 ResetTimer
和/或 StopTimer
调用来包围基准测试循环。这些很容易忘记,即使开发者记得它们可能是必要的,也很难判断设置或清理是否“足够昂贵”以至于需要它们。
没有这些,testing
包只能计时整个基准测试函数。如果基准测试函数省略了它们,设置和清理代码将包含在总时间测量中,悄无声息地扭曲最终的基准测试结果。
还有另一个更微妙的陷阱,需要更深入的理解:(示例来源)
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
的循环,并防止循环内的死代码消除。在 Go 1.24 中,这是通过禁止内联到此类循环的主体中实现的,但我们计划在将来改进这一点。
testing.B.Loop
的另一个优点是其一次性启动方法。对于 b.N
风格的基准测试,testing 包必须多次调用基准测试函数,并使用不同的 b.N
值,直到测得的时间达到阈值。相比之下,b.Loop
可以简单地运行基准测试循环,直到达到时间阈值,并且只需调用基准测试函数一次。在内部,b.Loop
仍然使用启动过程来分摊测量开销,但这对于调用者是隐藏的,并且效率更高。
b.N
风格循环的某些限制仍然适用于 b.Loop
风格循环。如有必要,用户仍然有责任在基准测试循环中管理计时器:(示例来源)
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
方法现在是编写基准测试的首选方式
func Benchmark(b *testing.B) {
... setup ...
for b.Loop() {
// optional timer control for in-loop setup/cleanup
... code to measure ...
}
... cleanup ...
}
testing.B.Loop
提供更快、更准确、更直观的基准测试。
致谢
非常感谢社区中所有为提案问题提供反馈并在该功能发布时报告错误的人!我也感谢 Eli Bendersky 提供的有益博客摘要。最后,非常感谢 Austin Clements、Cherry Mui 和 Michael Pratt 对设计选项和文档改进的审查和深思熟虑的工作。感谢大家的所有贡献!
下一篇文章:Go 加密安全审计
上一篇文章:再见核心类型 - 你好我们熟悉和喜爱的 Go!
博客索引