Go 32:逃逸诊断为什么不是分配账本
上篇把造输入排除在计时循环外,记录了 CountWords 从 strings.Fields 换成逐 rune 扫描前后的分配和时间。累计工程没有换服务:这里仍比较同一个统计函数,并且把编译器对这个函数的判断和实际 benchmark 并排看。工程与证据包、SHA-256、独立复跑步骤可重跑。
同一个函数,两个版本的编译诊断
旧版来自固定提交 351b838365057d66f7a845801469bc267ddb4055 的 examples/go-language/stats/count.go,函数只有一行:return len(strings.Fields(text))。在新临时目录解出该提交的整个模块,和当前模块分别运行 go build -gcflags='example.com/go-language-task/stats=-m=2' ./stats。指定包匹配避免把依赖的诊断淹没在输出里。标准输出都为空,编译器将优化信息写到标准错误;两个命令都退出 0。冻结 Go 1.27.1 的 go doc cmd/compile 明确 -m 打印优化决策,这不是语言规范要求的固定文案。
旧函数一组原始消息是:
1 | |
新实现只扫描字符串中 rune 的空白边界,不再构造字段 slice。同一参数的诊断变为 stats/count.go:15:17: text does not escape;但同时也有 cannot inline CountWords: function too complex: cost 91 exceeds budget 80。优化减少了当前基准的分配,却让该函数在这次编译中无法内联。这两个变化是同一个输入和编译配置下的实现观察,不是 Go 语言对所有程序的栈/堆许诺。别只抄最后一句 text does not escape:旧函数是因为传给 strings.Fields 才出现参数泄露路径;“参数泄露”也没有证明字符串底层字节必然被复制到堆上。
编译判断与分配测量要双向核对
第 31 章的旧提交与改动后各跑五次,证明统计循环从 12,032 B/op、4 allocs/op 降到 0 B/op、0 allocs/op。为了排除“两个版本基准体不同”,本章又在累计工程的 stats/count_bench_test.go 给同一输入加上 BenchmarkTaskFieldsReference:循环内实际计算 len(strings.Fields(text)),用当前 CountWords 给出循环外预期,并逐次验证相等。没有复制一套任务工程,也没有改 HTTP 或 SQL 入口。
| 当前五轮 | ns/op 范围 | B/op | allocs/op |
|---|---|---|---|
BenchmarkTaskCountWords 逐 rune 扫描 |
5,795–7,116 | 0 | 0 |
BenchmarkTaskFieldsReference 切分字段 |
15,438–16,362 | 12,032 | 4 |
这是新版本构建的同负载算法对照,不是旧提交原二进制在同一时刻的计时。旧提交的实际基线另存于第 31 章证据;运行环境变动会让速度区间变动,所以不能据这张表推算生产任务吞吐。0 B/op 只是这个基准的每次调用平均分配观察,不能推出底层字符串没有存活成本、测试自身无需内存,或所有 AnalyzeContext 调用都不分配。旧版诊断说 CountWords 可内联,也不意味着它被调用时没有堆分配:这里实际的 Fields 对照仍报告 4 次分配。
如果想从日志推断某个任务请求用了多少内存,应先确认业务调用路径。当前 worker 使用带 context 的 AnalyzeContext,而 HTTP GET 读取已完成任务;这两条路径都不会凭 CountWords 的编译诊断自动获得性能结论。并发排队、DB 结果集和 HTTP 响应还有各自的存活对象。-gcflags 的包匹配、编译器版本、调用位置、内联决定与测试计时范围一旦变了,就需要重新保留两份日志;仅把源码里的局部变量标成“栈对象”既不能验证,通常也会误导。
练习:① 旧实现编译器输出 can inline CountWords,能否推断旧版无堆分配?不能,本章同输入参考基准仍有 12,032 B/op、4 allocs/op;还要看 strings.Fields 的返回对象和实际调用。② 新实现 text does not escape 是否等于字符串数据在栈上?不能,这句描述本次函数形参的逃逸信息,不是字符串所有者存储位置的证明。③ 如果引入新包装函数后数字不同,先改结论还是先重测?先固定调用点与负载、保存新编译诊断和基准原始输出,再比较;原结果仅适用于此前配置。
真实临时 PostgreSQL 16.15 的服务回归、两包测试、实际 -race 路径和旧 CLI 的正常/拒绝输入都保留在 writing-plans/go-language/evidence/32/;完整包在新目录、新数据库、新缓存单独复跑。无 race 报告只说明执行路径没报竞争,不能证明所有路径安全;HTTP 202 不等于任务完成,数据库提交不等于外部副作用完成。远程网络、其他平台、含 32 的全站构建与发布仍 NOT_RUN。下一篇把分配速率、存活堆和 GC 配置放到同一负载下测量。
参考资料
- 冻结 Go 1.27.1
cmd/compile的-m参数;本次go doc cmd/compile原文在evidence/32/doc-compile.*。 - 冻结 Go 1.27.1
go help build的-gcflags包匹配,本次实际输出在evidence/32/doc-build.*。






