Go benchmark 怎样避免编译器优化掉被测代码
来源:17golang原创
时间:2026-10-14 04:28:11 324浏览 收藏
Go benchmark 最容易被忽略的一类问题,是循环看起来执行了很多次,实际测到的却不是目标函数。典型代码把返回值赋给局部变量后立即丢弃,编译器发现这段计算没有可观察副作用,就可能把它简化甚至删除。解决办法是:Go 1.24 及以后优先使用精确写法 for b.Loop();旧版本继续使用 b.N 时,则把返回值汇总到包级变量。
b.Loop()的条件必须原样写在 for 中,循环体内的调用结果会被 testing 保活。- 旧式
b.N不能只把结果放进循环内的临时变量,使用包级 sink 才能让计算保持可观察。 - 输入准备放在计时前;旧式写法用
ResetTimer,多次运行再用 benchstat 比较。
先判断:基准循环是不是只剩空调用
假设要测一个返回整数的哈希或解析函数,下面的写法表达了“我关心函数耗时”,但没有表达“返回值必须留下来”。如果函数没有 I/O、全局写入或其他可见副作用,优化器就有机会认为调用结果不影响程序。
package demo
import "testing"
func BenchmarkHash(b *testing.B) {
for i := 0; i
这里的关键不是“每次都要打印结果”。打印会改变被测对象;正确做法是保存结果,但不要把日志、分配或锁竞争混进计时区域。
用 b.Loop 保住循环体的工作量
当前 testing 文档推荐新基准使用 B.Loop。它不只是把 b.N 换成另一种循环:在 for b.Loop() { ... } 的大括号内,函数调用的参数、返回值和赋值变量会被保活,避免被测调用被完全优化掉。条件必须精确写成 b.Loop(),不要包一层自定义函数或改成等价表达式。

func BenchmarkHashLoop(b *testing.B) {
input := "go benchmark"
for b.Loop() {
// b.Loop 会让循环体内的调用和返回值保持可观察。
fastHash(input)
}
}
func BenchmarkHashLoopUsed(b *testing.B) {
input := "go benchmark"
for b.Loop() {
// 显式接住结果,便于表达测试意图和后续扩展统计。
got := fastHash(input)
_ = got
}
}
第二个示例里的 _ = got 主要是说明意图;在支持 b.Loop 的 Go 版本中,真正重要的是循环条件和循环体位置。不要把 b.Loop 与 for i := 0; i 同时套在一个 benchmark 里。
旧式 b.N 写法为什么需要结果汇总
如果项目还要兼容没有 B.Loop 的 Go 版本,可以保留 b.N,但让结果流向包级变量。这样结果具有跨函数调用仍可观察的存储位置,编译器不能把整段计算简单视作“完全没用”。这个 sink 不应在并行基准之间共享可变状态,也不要在循环里打印。

package demo
import "testing"
var benchmarkSink uint64
func BenchmarkHashN(b *testing.B) {
input := "go benchmark"
b.ResetTimer()
for i := 0; i
把计时边界和结果可信度一起复查
基准修好后,先只比较同一台机器、同一输入和同一编译配置下的结果。b.Loop 会自动处理迭代与计时;b.N 版本则要确认循环确实执行了 b.N 次,且 setup 没有被意外算进耗时。
| 检查点 | 正确做法 | 常见误区 |
|---|---|---|
| 循环条件 | 新代码写成 for b.Loop() | 与 b.N 双重循环,或封装后再调用 |
| 结果去向 | Loop 内保活;旧式写法用包级 sink | 只赋给循环内临时变量后丢弃 |
| 输入准备 | 计时前构造,旧式写法随后 ResetTimer | 把 make、读文件和解析初始化混在循环中 |
| 重复运行 | go test -run '^$' -bench BenchmarkHash -count=5 后用 benchstat | 只看一次 ns/op 就下结论 |
# 只运行目标 benchmark,重复采样以减少偶然抖动。 go test -run '^$' -bench '^BenchmarkHash' -count=5 # 对两组已保存的基准结果做统计比较。 benchstat before.txt after.txt
命令中的正则只负责筛选测试名;它不能证明实现一定没有被优化。最终仍要回到代码检查:目标调用是否位于计时循环内、返回值是否有存活路径、setup 是否被排除、输入规模是否一致。
相关问题
b.Loop 能不能和 b.N 一起使用?
不能。一个 benchmark 选择其中一种循环方式即可;新代码优先使用 b.Loop,旧代码才继续按 b.N 编写。
为什么不建议在 benchmark 里 fmt.Println 结果?
输出会带来格式化、系统调用和锁等额外成本,改变被测路径。用包级 sink 或 b.Loop 的保活语义即可保留结果。
ResetTimer 什么时候需要?
旧式 b.N benchmark 在循环前做了昂贵初始化时使用它;如果初始化本身就是要测的对象,则不要为了好看而排除它。
-
275 收藏
-
257 收藏
-
151 收藏
-
101 收藏
-
323 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习