Go 31:benchmark 为何不能只测准备工作
上篇用有界 fuzz 寻找任务接收与状态序列的反例。这一次要回答另一个容易混淆的问题:数字下降了,究竟是计数函数更快,还是测量时少做了一部分工作?沿用同一个任务服务;累计工程包、SHA-256及独立复跑步骤保留完整源码与原始命令。
准备一次任务,不等于执行一次任务
原先的 CountWords 用 len(strings.Fields(text)) 计算词数。如果基准把 strings.Repeat 生成输入也放进计时循环,数字同时包含造字符串、分配和扫描。服务的 HTTP 查询又是另一条路径:已完成任务的 GET 会读取存储、编码 JSON,不会调用 CountWords。把这两种耗时混在一个基准里,优化统计函数后看到的请求耗时,既不能归因,也不能检验 HTTP 是否受益。
冻结版 Go 1.27.1 的 testing.B.Loop 会在第一次调用时重置计时器,循环结束停止计时。因此两个基准都在循环外造输入和服务实例,在循环内执行要测的操作及最小业务断言:
1 | |
断言仍在循环体内,所以纳秒数包含比较和分支;但断言用的是循环外预先算好的目标,不会重复分配字段切片。另一个 BenchmarkTaskHTTPRead 事先向当前任务存储写入 SUCCEEDED,循环中用 httptest.ResponseRecorder 对原 handler 发 GET,同时检查 200、终态和词数;每轮必须新建 recorder,否则上一轮的响应会污染结果。这一项包含 recorder 分配、路由和编码,不是只测 JSON 编码,更不是走真实网络的吞吐量实验。循环结束由 defer service.Close() 清理资源,清理不计入本轮数字。
相同负载上的两次比较
基线在旧 CountWords 实现下先执行测试,再以 -benchtime=200ms -count=5 -benchmem 跑两包基准;之后只把 CountWords 换成逐 rune 扫描并使用 unicode.IsSpace 切换词边界,保持 Unicode 空白定义与 strings.Fields 相同。固定输入、基准代码和 GOMAXPROCS=2 没变。原始五轮记录均在 writing-plans/go-language/evidence/31/:
| 路径 | 基线五次 ns/op | 改动后五次 ns/op | 基线 → 改动后分配 |
|---|---|---|---|
统计 CountWords |
17,916–25,880 | 5,954–6,446 | 12,032 B/4 次 → 0 B/0 次 |
| 已完成任务 HTTP GET | 3,728–4,746 | 3,433–4,057 | 1,232 B/14 次 → 1,232 B/14 次 |
在这台 linux/amd64 主机和这些输入上,统计函数减少了分配,五轮计时区间也下降;它并不意味着异步任务服务整体加速。HTTP GET 没有调用修改后的函数,其区间重叠;不能拿 4,746 与 3,433 两端相减就宣称服务性能提升。ReportAllocs 报告的是这段基准循环里的平均分配,不是进程一生的内存,也不反映数据库请求或 worker 队列的耗时。两种路径均不是生产负载;系统调度、缓存和工作负载变化仍会改变数字。
更快的实现必须仍然算同一道题
strings.Fields 按 Unicode 空白切分,逐字节扫描会把中文与多字节空白误判。新实现以 range 遍历 rune,按 unicode.IsSpace 判断空白,并只统计从空白到非空白的转换。TestCountWordsMatchesFields 把 ASCII、中文、组合字符、不间断空格、零宽字符、非法 UTF-8 和长字符串与旧定义逐一比较;FuzzCountWordsMatchesFields 在最多 4096 字节输入上差分搜索。本次 3 秒单 worker 的实际执行数为 4,408,未发现差异,不能把这个有限搜索写成对所有 Unicode 输入的证明。累计工程的真实 PostgreSQL 16.15 测试、两包回归和旧 CLI 正反仍须通过,单独的函数基准不替代它们。
练习:① 把 strings.Repeat 移进 for b.Loop(),测到的差额还能归因于 CountWords 吗?不能,输入构造也进了计时范围;保留原版基准作为对照,并单独量化构造成本。② HTTP GET 的数字下降,是否能归因于 CountWords?不能,这条 GET 只读已完成状态并编码响应,应该先画调用路径,再用相同请求负载做多轮对照。③ 0 B/op 能推出没有资源成本吗?不能,它只描述该测试计时段中报告的平均堆分配;运行时间非零,准备输入和清理也没有被这个数字计入。
一手 API 合同来自冻结 Go 1.27.1 的 go doc testing.B.Loop、ReportAllocs 与 strings.Fields,查询命令及双流/退出码、基线和优化后每轮原始输出、临时 PostgreSQL 的生命周期及版本见 writing-plans/go-language/evidence/31/。此处只引用公开 API 和测量,不给运行时源码行为下结论。没有 race 报告只限已执行路径;HTTP 202、数据库提交与外部副作用是否完成仍不可互换。远程数据库、其他平台、全站构建、发布仍 NOT_RUN。下一篇再用固定编译诊断对照这次的分配测量,检验逃逸分析能说明什么。
参考资料
- Go 1.27.1
testing.B.Loop和testing.B.ReportAllocs,本次冻结工具链原文见writing-plans/go-language/evidence/31/doc-*。 - Go 1.27.1
strings.Fields;本次差分实验同时使用unicode.IsSpace的冻结版文档。






