没有不值得去解决的问题,也没有不值得去学习的技术!

更快不一定更好:为什么我主动撤销了一次已经明显变快的性能优化

作者:

在

Go 学习与文档多语言本地化项目

图3:访问过去的 Go Tour 简体中文站 tour.go-zh.org 时,当前已经无法正常建立连接。

(1) 从「A Tour of Go 中文版」这个搜索词开始:我决定做一个持续维护的 Go Tour 中文版

本地运行的 A Tour of Go「Methods continued」课程页面,左侧为课程说明,右侧为 Go 代码编辑器及运行结果。

(2) A Tour of Go 中文版项目设计冻结:从 101 页到 103 页,从 Gin 转向 Cobra CLI

A Tour of Go 中文版开发实战:英文基线、在线运行与 101 页上游同步体系

(3) A Tour of Go 中文版开发实战:英文基线、在线运行与 101 页上游同步体系

go-tour-i18n 项目完成首个简体中文课程页面 welcome/1 的整页翻译、结构校验和本地预览。

(4) A Tour of Go 多语言翻译项目实录:完成首个 zh-CN 页面翻译闭环

图 1:DeepSeek 官方更新日志显示,本次只更新 DeepSeek-V4-Flash,DeepSeek-V4-Pro API 与 APP、Web 模型均未更新

(5) A Tour of Go 中文翻译项目进展:完成前 8 页、修复 present 语法,并暂缓 DeepSeek 对比

图 1:generics/1 简体中文页面本地预览

(6) 一个页面重试五次:使用 GLM-5.2 翻译 A Tour of Go 时遇到的 Token 与 Present 结构问题

图 1:methods/16 简体中文页面浏览器预览

(7) A Tour of Go 中文翻译实录:如何只翻译教学代码注释,而不破坏 Go 代码

图 2:A Tour of Go methods/20「练习:错误」中文候选页面最终渲染效果

(8) A Tour of Go 多语言翻译:当正确的中文语序被受保护标记顺序校验误判

图 3:methods/24 中文页面,静态代码、Bounds 行内代码和链接内的 image.Rectangle 均正确渲染

(9) A Tour of Go 中文翻译完成 7 个代表页校准:最后 3 页又发现了哪些真实问题

图 2:首批 10 个普通页面经过试跑与流程校准后全部进入 ready

(10) A Tour of Go 多语言翻译项目:首批 10 个普通页面试跑,从 2 个 blocked 到全部 ready

图 2:flowcontrol/6 最终中文页面,return 与 v 为适应自然中文语序发生整体换位

(11) A Tour of Go 中文翻译第二批实跑:从 Inline Code 顺序误判到历史响应重新验证

图 1:flowcontrol/6 首次原始输入实验直接通过统一自动校验

(12) A Tour of Go 多语言翻译项目:从 swap 问题重新审视 Token 保护,原始输入与最小保护的一次真实实验

图 7:moretypes/1 最终中文页面预览。左侧课程正文、静态代码块、行内代码和教学注释均正常渲染,右侧官方 Go 示例继续保持原样。

(13) A Tour of Go 多语言翻译实战:第三批 10 页全部 Ready,继续校准 Protected Token 的结构角色

图 4:上游源码确认共有 103 个课程页面,zh-CN 最终达到 ready=103、pending=0、blocked=0

(14) A Tour of Go 多语言翻译项目:zh-CN 103 个课程页面全部 ready,课程正文翻译阶段完成

图 4:GitHub 与 ChatGPT 已成功连接

(15) ChatGPT 连接 GitHub 实测:让 AI 直接读取 go-tour-i18n 仓库参与译文审核

图 3:中文 Go 术语校准完成后,将确定的译法写入 zh-CN glossary

(16) 为什么 channel 最终选择“通道”:A Tour of Go 中文术语统一的一次实践

图 1:技术含义基本正确,但“返回一个返回……”已经明显影响阅读,因此仍被列入 C 类修订

(17) 103 页逐页审核后,只返修 16 页:A Tour of Go 中文正文发布前质量审计

图 2:中文课程页面以及已经完成本地化的编辑器控制区

(18) A Tour of Go 多语言翻译项目:公共 UI 本地化完成,从课程译文走向完整中文界面

图 2:完整 zh-CN 正式投影成功生成,103 个课程页面被重新组装为 7 个 .article

(19) 从 103/103 ready 到完整站点验收:A Tour of Go 中文版补齐正式投影与课程元数据本地化

图 3:从 production bundle 独立启动的 A Tour of Go 简体中文页面,课程正文、公共 UI 和官方 Go 示例均已进入最终发布形态。

(20) A Tour of Go 多语言翻译项目:103 页全部完成后,我终于生成了可独立部署的 zh-CN Production Bundle

图 1:A Tour of Go 简体中文版正式生产页面,远程运行已经成功输出“Hello, 世界”。

(21) A Tour of Go 多语言翻译项目:zh-CN 正式上线,从生产发布到 go.dev Playground 的完整部署记录

图 1:新生产域名 go-dev.shuijingwanwq.com 已正常访问,Go 示例远程运行成功

(22) A Tour of Go 多语言翻译项目:从 go-tour 到 go-dev,完成 EdgeOne、Nginx 与 HTTPS 生产域名迁移

图 1:A Tour of Go 多语言翻译项目正式生产首页

(23) A Tour of Go 多语言翻译项目正式上线:从项目首页、公共 Footer 到生产发布元数据的完整收尾

图 3:GA4 g/collect 请求返回 HTTP 204,并携带当前课程页面地址,确认 Google Analytics 已产生真实事件上报

(24) A Tour of Go 多语言翻译项目:接入 Google Analytics 与百度统计,从 systemd 注入到 EdgeOne 与真实上报验证

图 1:生产环境 robots.txt 与 sitemap.xml 已正确对外提供,Sitemap 共包含 104 个规范 URL。

(25) A Tour of Go 中文站上线后:补齐 robots.txt、Sitemap,并完成五大搜索引擎提交

图 4:发布生产并刷新 EdgeOne 缓存后,再次通过真实手机访问,顶栏项目名称已经完整保持在一行,最终移动端验收通过。

(26) A Tour of Go 多语言翻译项目:修复手机端首页顶栏标题换行,并完成真机验证

图 3:生产自动部署脚本第一次真实运行时,systemd 已经显示 active,但首次 localhost 探测仍然得到 HTTP 000;随后连续 3 次同时满足 active + HTTP 200 后,脚本才判定部署成功,并继续完成公网 HTTP 200 验收。这次真实运行验证了连续应用层健康检查的必要性。

(27) A Tour of Go 多语言翻译项目:从一次 203/EXEC 故障到生产自动部署脚本落地

图 1:阿里云生产环境访问 go.dev Playground 时出现 TLS 握手超时

(28) A Tour of Go 多语言翻译项目:为 Playground 搭建独立的 ZgoCloud 执行代理

图 3:清除 /tour/script.js 缓存后,Run 已直接请求 ZgoCloud /compile 并返回 HTTP 200

(29) A Tour of Go 多语言翻译项目:将生产 Run / Format 切换到 ZgoCloud 执行节点

【图 1:微信中运行 Go 示例时出现 Error communicating with remote server.】

(30) A Tour of Go 中文版手机端运行与格式化偶发失败排查:从间歇性报错到补强 Nginx 可观测性

【图 4:向 golang-dev 公开邮件列表发送 Playground 使用告知邮件并投递成功】

(31) A Tour of Go 多语言项目:为 Playground 代理增加唯一 User-Agent,并主动告知 Go 官方

【图 3:百度剩余 9 个核心 URL 提交成功,remain 归零】

(32) A Tour of Go 搜索引擎收录实测:从 Google 自然索引到百度主动提交与 Bing IndexNow

【图 1:当前正式中文 methods/24 页面,右侧示例代码运行成功】

(33) A Tour of Go 中文翻译完成后,我为什么重新评估 minimal-protect 翻译模式

【图 3:5 个页面 Request SHA256 完全一致,但 Response SHA256 全部不同】

(34) A Tour of Go 翻译实验:更多原始上下文为什么没有更好?从 157 个保护标记到 30 次重复盲评

图 3:3 个代表页 × 4 个评审模型,共 12 次匿名评审的完整排名与第一名票数。

(35) A Tour of Go 翻译质量再评估:minimal-protect 未达预期后,我开始比较 ChatGPT、Codex 与 GLM-5.2

图 2:GitHub 中的 chatgpt-zh-CN-008 重译批次。一个 Batch 同时保存 inputs、raw-responses、candidates、validation 和 retry 记录,GitHub 成为 ChatGPT 与本地翻译流水线之间的数据交接层。

(36) 没有 OpenAI API,我如何用 ChatGPT + GitHub 完成 A Tour of Go 103 页全量重译

图 1:103 个正式课程页面完成最终语义质量审核,结果为 A=103、B/C/D=0,同时不存在缺失、重复或额外页面。A/B/C/D 是本轮项目内部语义质量分级,并不是自动结构 validator 的结果。

(37) 103 页翻完还不能发布:A Tour of Go ChatGPT 译文的语义审核与 Canonical Promotion

图 3:7 个 article endpoint 的 production origin 与 EdgeOne 公网响应全部 exact match,同时新的 ChatGPT 译文标记存在,旧版译文标记已经消失。

(38) 103/103 与 7/7 Exact Match:A Tour of Go ChatGPT 新译文的最终线上验收

【图 2:upstream source preview】

(39) A Tour of Go 多语言翻译项目首次实现 upstream 同步机制:从一次性翻译到长期维护

图 2:A/B/C/D 质量评级规则,展示正式质量 rubric 与 Promotion gate。

(40) A Tour of Go 多语言翻译项目引入 Translation Quality Review:从自动验证到 AI 翻译质量控制

图 4:翻译后 Playground Example,展示中文注释与保持不变的 Go 代码结构。

(41) A Tour of Go 多语言翻译项目实现 Playground Example 本地化:从代码注释翻译到完整学习体验

图 5:公网页面验证广告脚本输出

(42) A Tour of Go 多语言翻译项目接入广告系统:从 AdSense 专用配置到通用 HTML 注入架构

图 3:百度统计单页应用设置

(43) go-dev 单页网站接入统计与 Google AdSense Auto Ads 验证记录

图 1:GitHub 仓库中的多语言翻译项目结构

(44) A Tour of Go 多语言翻译项目:选择日语作为第二语言并启动翻译验证

图 5:模型身份描述不一致

(45) A Tour of Go 多语言翻译项目中的 AI 协作异常排查:一次长期工程会话失效分析

【图 1:ChatGPT GitHub 写入流程调整说明截图】

(46) go-tour-i18n 翻译流程调整:从 ChatGPT GitHub 写入到本地 artifact 导入

图 4 展示了本次异常状态。

(47) go-tour-i18n 翻译自动化流程复盘:已验证的 ChatGPT 翻译能力在新任务中的稳定性问题

【图 6:全部评审结束以后才读取 secret key,完成最终揭盲】

(48) A Tour of Go 翻译质量再评估:Codex High 已接近 ChatGPT High,我决定调整默认翻译引擎

图 4:ja-JP 生产课程页实际运行 Go 示例

(49) A Tour of Go 日语版正式上线:第二门语言 ja-JP 完成生产发布

图 1:ja-JP 上线以后,先提交“新增语言标准化流程”

(50) A Tour of Go 多语言扩展复盘:为第三门语言建立标准化流程

图 1:课程正文结束以后,页面左侧仍然存在大块视觉空白,但 AdSense 并没有在这里插入展示广告。

(51) A Tour of Go 明明有大片空白,为什么 AdSense 就是不显示展示广告?

图 2:golang/website 当前的 _content/tour/。课程内容、静态资源和模板都在这个上游目录中持续维护。

(52) 为了 AdSense,要不要放弃 SPA?A Tour of Go 页面架构的一次取舍

图 4:最终的手动课程广告真实展示在课程内容区域内,而不是 footer 之后。

(53) 保留 SPA 以后,广告应该放在哪里?从 AdSense 预览到方案 B

图 1:Google Search Console 中,多个不同的 A Tour of Go 课程 URL 被判断为“重复网页,用户未选定规范网页”。

(54) SPA 的 SEO 老问题:为什么最后还是给 103 个 A Tour of Go 课程页生成了 Prerender HTML

图 2:方案 B 生产环境中,源码已经正常,但课程主体高度异常缩短,footer 提前进入第一屏。

(55) 从源码空白到 AdSense 高度污染:A Tour of Go 方案 B 的生产收尾

图 1:A Tour of Go 德语版 de-DE 已正式运行在生产环境

(56) A Tour of Go de-DE 上线复盘:第三门语言用了约 16 小时,下一门如何压到 8 小时?

图 1:fr-FR 首次 production 验收完成后,我没有立即开始 ko-KR,而是先连续完成新增语言与首次生产流程的优化

(57) 法语版上线后,我没有马上开始韩语:A Tour of Go 新增语言流程的一轮完善与简化

图 1:根据 ko-KR 期间 76 次 Git 提交估算实际工作时间

(58) 原本预计 6 小时,最后用了约 16~18 小时:A Tour of Go 韩语版上线复盘

图 2:英文 source 与当时发生 validation failure 的 ko-KR candidate 对比

(59) Validator 报错,到底该改译文还是改规则?一次 A Tour of Go 韩语翻译实战

图 2:法语 A Tour of Go 仓库目前已经归档,页面仍保留 553 Commits 和 61 Contributors 的历史

(60) 从法语 553 次提交到韩语两代离线:A Tour of Go 社区翻译为什么难以长期维护

【图 4:es-ES 首次 Production 收口后连续完成的流程优化 commit】

(61) 从 es-ES 首次上线复盘:我把 A Tour of Go 新增语言的 Production 流程又收敛了一轮

图 1:旧公网验收链路连续出现 curl 28 超时,最终导致 public-machine 阶段失败

(62) A Tour of Go Production 公网验收踩坑:从跨境 SOCKS 链路改为 zgocloud direct

图 1:两次 Production Machine Acceptance 都已经通过,但 Headless Chrome 分别在 / 和 /tour/ 判定页面没有完成渲染

(63) 页面明明能打开,为什么 Headless Chrome 仍然判定 Production 失败?

图 3:Bing 从 Google Search Console 中识别到新的 it-IT 站点,并同时发现已有的 1 个 sitemap

(64) GSC → Bing Webmaster Tools:多语言站点如何减少重复验证

图 3:英文站 3 小时内出现 565 个 Cloudflare 来源 URL

(65) IndexNow 实战:手工提交长期不显示,Cloudflare Crawler Hints 却很快出现在 Bing Webmaster Tools

【图 1:A Tour of Go 项目首页目前的语言版本列表】

(66) 从 nl-NL 到 tr-TR:A Tour of Go 连续上线三门语言后,我又优化了哪些流程

【图 4:Go local 页面中已经出现 French、German、Korean、Simplified Chinese】

(67) 从邮件无人回复到进入 Go 官方 Go local:我的 4 个 A Tour of Go 翻译站终于被收录

图8:最终只保留一把启用的新 API Key

(68) 腾讯云 EdgeOne API 自动化:从 CAM 子用户到最小权限 API Key 的完整配置

【图 7:Production Release Batch 最终汇总】

(69) 从同步 Go 官方上游到一个命令发布 10 个语言站:A Tour of Go Production 流程的再次收敛

【图 3:Cloudflare Smart Tiered Cache 已启用】

(70) Cloudflare 缓存 HIT 很快,MISS 却不稳定:回源阿里云杭州的一次真实优化

【图 3:简体中文站 Support 页面】

(71) 广告之外,社区项目如何长期活下去?我给 A Tour of Go 多语言项目加了 Support

图5:评论成功发布

(72) 从 Go Weekly 到 Reddit、DEV 和 Go Forum:第一次系统推广 A Tour of Go 多语言项目

【图 4:RTL 桌面课程布局修复后的效果】

(73) 第一次把 A Tour of Go 做成 RTL:阿拉伯语版本上线,以及我踩过的几个坑

多语言项目应该先做哪些语言?我的 66 个语言版本商业优先级排序

(74) 多语言项目应该先做哪些语言?我的 66 个语言版本商业优先级排序

当术语一致性遇到性能瓶颈:Go 多语言翻译项目的一次架构取舍

(75) 当术语一致性遇到性能瓶颈:Go 多语言翻译项目的一次架构取舍

从 A Tour of Go 走向 Go Learning & Documentation:为什么这个多语言项目需要 v2

(76) 从 A Tour of Go 走向 Go Learning & Documentation:为什么这个多语言项目需要 v2

Markdown 明明可以拆成很多段,为什么 v2 仍然坚持一个 Page 就是一个 TranslationUnit

(77) Markdown 明明可以拆成很多段,为什么 v2 仍然坚持一个 Page 就是一个 TranslationUnit

Tour、Learn、Docs 为什么不应该各维护一份术语表:v2 的统一 Glossary 设计

(78) Tour、Learn、Docs 为什么不应该各维护一份术语表:v2 的统一 Glossary 设计

v2 术语表变更后的页面影响范围判断流程:区分 Compatible 与 Affected,只处理真正受影响的页面。

(79) 术语表改了,为什么不应该让所有页面重新翻译?v2 的影响范围判断

CLI 性能优化示意图:通过 Prepare Once、Reuse Many Times,将执行时间从 6m54.554s 降至 10.335s,约提升 40.1 倍。

(80) 从 6 分 54 秒到 10.3 秒:Go 多语言项目一次 CLI 性能优化实战

Markdown Page 经过 Parse / Protect、Generation 和 Reconstruct / Validate 的流程示意图,强调保护 inline code、链接、代码等机器语义,同时允许译文按照自然语言调整语序。

(81) Markdown 不是纯文本:Learn / Docs 为什么需要新的 parser 与结构保护

历史为什么值得保留:复用已经完成的翻译成果,而不是重新做一遍

(82) 历史为什么值得保留:复用已经完成的翻译成果,而不是重新做一遍

历史状态通过 Trusted Checkpoint 建立可信断点,旧记录继续保留用于审计和诊断,后续工作流从当前状态继续向前,不再要求无限重建过去。

(83) 历史应该被保留,而不是被反复重建:v2 为什么最终需要一个“断点”

v2 依赖与失效传播流程图:从内容变化开始,评估依赖关系和真实影响,将结果区分为“仍然有效”和“需要失效”,只让真正受影响的状态重新进入流程。

(84) 一次内容变化,到底应该让哪些状态失效?v2 的依赖与失效传播设计

v2 本地内容覆盖判断流程图:官方英文页面先检查是否已有正式本地覆盖,有覆盖时使用本地 URL,没有覆盖时直接链接 go.dev,并保持本地路径真实 404。

(85) 有英文页面,不代表一定应该有本地页面:v2 如何处理本地内容覆盖、canonical 与真实 404

v2 代码注释完整性审计流程图:从源页面建立代码块清单,分类教学注释、机器语义和无法判断的内容,再与当前译文精确对应,确认 missing、extra 和 protected mutation 均为 0。

(86) 代码块里的注释为什么不能只靠肉眼检查?v2 如何统计并验证代码注释翻译完整性

结构化语义输入扩展示意图:同一份 books.yaml 原始文件保持不变,旧 parser 识别 10 个可本地化单元,当前 parser 识别 12 个,并新增 PDF 和 GitHub,确认旧单元无删除无修改后重新进入 Generation。

(87) 原始文件一个字没变,为什么还要重新翻译?从 books.yaml 10→12 看语义输入扩展

v2 Activation 流程图:Surface PASS 后先执行只读 activation-preflight 获取 prior_sha256,activation-apply 时通过 CAS 再次比较当前状态,只有 current 等于 prior 才提交 result_sha256,否则停止并重新 preflight。

(88) 审核都通过了,为什么上线前还要再检查一次?v2 的 Activation、prior SHA 与 CAS 设计

v2 分层验证流程图:TranslationUnit、Package Closure 和 Integrated Surface 均通过后进入 Activation,再由 Preview 检查 LoadContent、Compose、Resolver 和 Renderer 的真实运行链;如果实际使用错误 renderer,即使前置内容全部通过,Preview 仍然失败。

(89) Surface 已经 PASS,为什么 Preview 还是会失败?从一次 wrong renderer 看分层验证的价值

更快不一定更好:为什么我主动撤销了一次已经明显变快的性能优化

(90) 更快不一定更好:为什么我主动撤销了一次已经明显变快的性能优化

最近这段时间,在 Go Learning & Documentation v2 的开发里,我花了不少时间处理性能问题。

原因很现实。

随着工作流越来越严格,一条命令背后可能同时需要检查:

  • 当前内容包;
  • 历史证据;
  • source identity;
  • candidate identity;
  • glossary;
  • Snapshot;
  • Independent QC;
  • package closure;
  • Integrated Surface;
  • currentness;
  • dependency;
  • SHA-256;
  • stale detection。

这些检查本身都有意义。

但如果同一份确定性数据在一次命令里被反复读取、解析和验证,执行时间就会越来越长。

有些低频命令最开始甚至需要几分钟。

于是自然会出现一个目标:

把它优化快一点。

问题是,性能优化做到后面,我逐渐发现另一个更难回答的问题:

到底应该优化到多快,才算够?

这次有一轮优化尤其典型。

同一条只读规划路径,基线实测大约是:

Plaintext
优化前耗时:164.03 秒

经过一轮相对保守的修改以后,降到了:

Plaintext
优化后耗时:125.65 秒
耗时下降:约 23.4%

这已经是一个不错的结果。

但后来继续推进更激进的方案时,我一度把同类路径压到了大约:

Plaintext
最快实测:35.93 秒

从数字上看,这显然更漂亮。

问题是:

最后我还是把这部分优化撤销了。

因为性能数字虽然更好,但正确性证明没有一起成立。

这篇就整理为什么我最后选择:

保留没那么极致、但更简单和更容易证明正确的优化。

而不是把“35 秒”当成必须保住的成绩。


一开始,性能确实已经成为真实问题

这套项目早期很多命令的工作量还比较小。

例如只处理 Tour 时,正式内容范围比较稳定。

后来扩展到:

  • Site shell;
  • Learn;
  • Docs;
  • structured content;
  • Integrated Surface;
  • Preview;
  • future content packages;

以后,一个看起来只是:

“重新计算当前范围”

的命令,背后已经可能需要穿过很多正式 authority。

如果每经过一层都重新:

  • 读文件;
  • 解 JSON;
  • 构建 catalog;
  • 计算 SHA;
  • 验证 historical reference;
  • 检查 current state;

耗时自然会不断叠加。

所以性能优化不是为了:

benchmark 好看。

而是因为维护工作已经开始受到实际影响。

一条低频但经常要人工等待几分钟的命令,会明显拖慢整个 workflow。


但性能优化不能先从“加缓存”开始

看到重复计算以后,最容易想到的办法通常是:

加 cache。

例如:

  • memoization;
  • LRU;
  • persistent cache;
  • global cache;
  • decoded object cache;
  • historical validation cache。

这些办法从纯性能角度看都很诱人。

因为如果:

Plaintext
第一次验证很贵

那么最直接的想法就是:

Plaintext
第二次不要再验证
→ 直接复用第一次的结果

问题在于,这套项目最核心的能力之一恰恰是:

发现“现在”和“刚才”已经不是同一个状态。

也就是:

  • currentness;
  • stale detection;
  • dependency mutation detection;
  • identity validation。

如果缓存设计不够严格,它最容易优化掉的,恰恰就是这些检查。


“上次 PASS”不能自动变成“这次仍然 PASS”

假设一个文件刚才验证通过:

Plaintext
10:00:文件 SHA 正确
10:00:validation PASS

到了 10:03 再次执行命令。

如果缓存逻辑只是:

这个路径以前已经 PASS,直接复用。

那么中间只要文件发生了变化,就可能完全检测不到。

对于普通工具,这可能只是缓存不够新。

但对于当前工作流,这可能意味着:

stale 内容被当成 current。

或者:

已经变化的依赖继续复用旧授权。

所以正式 currentness 检查不能被简化成:

“我以前见过这个对象。”

真正需要证明的是:

我现在重新读取到的正式 authority,仍然和被证明过的 exact bytes 对应。


所以我后来把缓存分成了两类

第一类是相对安全的:

单次 invocation 内,对已经拿到的 exact bytes 做确定性转换复用。

例如同一次命令已经读取了一份完全相同的 bytes。

接下来需要:

  • decode;
  • parse;
  • 构造一个纯确定性对象。

这种结果通常可以在当前 invocation 内复用。

因为它不会声称:

磁盘没有变化。

它只是说:

对同一份已经读取到的 exact bytes,不需要做两次相同的纯计算。

这是一个很重要的区别。


另一类缓存风险就高得多

另一种是:

缓存“磁盘现在是什么状态”。

例如缓存:

  • 某路径已经 current;
  • 某 closure 已经 PASS;
  • 某 dependency 已经验证;
  • 某 authority 上一次没有变化;
  • 某 Snapshot 上一次仍然有效。

这种缓存一旦跨越 invocation,问题就复杂很多。

因为它必须回答:

缓存以后,哪些依赖可能发生变化?

变化以后,谁负责让缓存失效?

有没有间接依赖没有纳入 key?

文件内容没变,但 parser authority 变了怎么办?

source 没变,但 semantic inputs 变了怎么办?

前面 books.yaml 10→12 的案例其实已经证明:

依赖并不只是文件 bytes。

所以跨 invocation cache 的 invalidation,比最开始想象的难很多。


最先做的事情应该是 profile,而不是猜

这一轮性能优化里,我后来比较认可的一点是:

先测,再改。

不是看到某个函数名字很复杂,就猜它一定慢。

而是先获得:

  • wall-clock;
  • CPU profile;
  • 实际 invocation 次数;
  • 重复调用路径;
  • real fixture。

然后再看:

时间到底花在哪里。

因为很多看起来“很重”的步骤,实际只执行一次。

反而一些看起来很小的 helper,可能在同一个 graph 里被重复调用几十次。

如果不 profile,很容易优化错方向。


第一个高价值目标:消除明显重复工作

真正值得优先处理的,通常不是“所有重复计算”。

而是:

明显、高频、占总时间比例很大的重复工作。

例如同一次 command invocation 里:

Plaintext
构建一次完整 validation graph
→ 后续多个阶段又重新构建相同 graph

这种情况下,如果输入仍然是同一组已经验证的 current bytes,就很适合做:

invocation-local reuse。

因为收益大。

而 correctness contract 又比较容易说明。

这种优化的特点通常是:

实现简单,收益明显,失效边界清楚。

这是我后来最愿意保留的一类优化。


第二个高价值目标:不要重复解析确定不会变化的历史表示

工作流里保存了大量历史证据。

历史证据之所以存在,就是因为:

它们已经成为 immutable history。

但“历史不可改”不代表:

每经过一个 helper,都要重新用不同路径完整解析一次。

如果在同一次正式验证流程中:

  • exact historical bytes 已经读取;
  • SHA 已经核对;
  • 引用 identity 已经确认;

那么后续纯转换就可以尽量复用已经得到的结果。

重点仍然是:

复用解析结果,而不是跳过 authority validation。

这个边界很关键。


第三个高价值目标:先做便宜的拒绝

还有一种很简单但很有效的优化:

调整 predicate 顺序。

假设一个对象最终需要检查:

Plaintext
先检查 cheap exact mismatch
→ 再检查昂贵历史图
→ 最后做完整重建

如果第一步已经能够确定:

identity 不匹配。

后面的昂贵验证就没有必要继续做。

相比建立复杂 cache,这类调整通常有几个优势:

  • 代码简单;
  • correctness 容易证明;
  • 不引入新的长期状态;
  • 不增加 invalidation 问题。

所以我越来越倾向于:

先找这种简单收益。


为什么 35.93 秒最后还是被撤销

后来更激进的优化把时间压得非常低。

从:

Plaintext
164.03 秒

到:

Plaintext
35.93 秒

单看数字,已经接近:

快了将近一个数量级。

如果这是普通 benchmark,很容易觉得:

这肯定应该合并。

但正式 workflow 不能只看 benchmark。

还要回答:

输出是不是完全一样?

currentness 有没有削弱?

dependency mutation 是否仍然能发现?

stale input 是否仍然拒绝?

targeted negative tests 是否继续失败?

historical integrity 是否仍然完整?

只要其中任何一个关键问题回答不了:

速度再快也不能叫 PASS。


一次 targeted test FAILED,可能比快一分钟更重要

性能优化最危险的地方是:

happy path 往往最容易变快。

因为正常输入通常:

  • current;
  • 完整;
  • 没有 mutation;
  • 没有 stale;
  • 没有并发变化。

缓存很容易在这种路径上表现得非常漂亮。

但真正重要的反而是 negative path。

例如:

dependency 已经变化。

source identity 不一致。

prior evidence stale。

historical bytes 被修改。

current file 在两次检查之间改变。

这些情况下,系统必须拒绝。

如果优化以后:

Plaintext
正常 fixture:PASS,而且特别快
异常 fixture:本来应该 FAIL,现在却 PASS

那这不是:

一个带小 bug 的高性能实现。

而是:

正确性 contract 已经被破坏。

所以这一类优化必须撤销。


更快,但证明成本也可能高很多

还有一种情况比较微妙。

优化本身不一定已经证明错误。

但它会引入非常复杂的证明负担。

比如一个多层 cache 可能需要维护:

Plaintext
cache key
→ source identity
→ parser identity
→ glossary identity
→ authorization identity
→ dependency graph identity
→ historical reference identity
→ mutation-sensitive invalidation

这时候真正需要评估的就不是:

它还能省多少秒?

而是:

为了证明这几十秒优化永远不会返回 stale state,需要增加多少代码和多少测试?

如果答案是:

要引入一套新的 cache architecture。

那对于一个低频 deterministic workflow 来说,很可能已经不划算。


性能收益也有边际递减

假设一个命令原来需要:

Plaintext
大约 10 分钟

优化到:

Plaintext
大约 2 分钟

这通常非常值得。

维护者每次执行都能明显感受到差别。

但如果再从:

Plaintext
2 分钟

继续优化到:

Plaintext
1 分 30 秒

价值就要重新评估。

如果这个 30 秒需要:

  • 大量缓存代码;
  • 复杂失效协议;
  • 新的 concurrency 考虑;
  • 更多测试;
  • 更高维护成本;

那么:

不优化反而可能是更好的工程决定。


“还能优化”不等于“值得优化”

这是我后来给项目补的一条很重要的原则。

性能任务不应该以:

Plaintext
REMAINING_DUPLICATE_WORK=none

作为结束条件。

因为现实里几乎永远都还能找到重复工作。

真正更合理的结束条件是:

Plaintext
REMAINING_HIGH_VALUE_OPTIMIZATION=none

也就是说:

不是“已经没有任何重复计算”。

而是:

已经没有值得继续花工程成本处理的高价值瓶颈。

这两个标准完全不同。


有些低收益重复计算应该允许存在

这一点开始会有点反直觉。

既然已经知道:

某一步会重复执行。

为什么不把它消掉?

因为消除重复计算本身也是有成本的。

假设一个重复检查每次只需要:

Plaintext
100 毫秒

但为了避免它,需要:

  • 新增 cache;
  • 管理生命周期;
  • 增加 invalidation;
  • 添加更多 identity;
  • 写十几个测试。

那么保留这 100 毫秒,可能反而是最便宜、最安全的方案。

所以:

重复计算不是天然的 bug。

只有当它造成实际高成本,而且能够用简单方式安全消除时,才值得优化。


复杂 cache 还会增加未来修改成本

性能优化还有一个容易被低估的问题:

未来维护。

今天加一个 LRU,看起来只是几十行代码。

但以后只要新增一个正式 dependency,就必须继续回答:

它是否进入 cache key?

旧 cache 是否自动失效?

historical reader 是否受影响?

current checker 是否还会重新读 disk?

如果漏掉一个 dependency,系统可能不是立即报错。

而是:

静默返回旧结果。

这类错误往往比“执行慢一点”更难发现。

所以我后来越来越倾向于:

宁可多算一次,也不要多维护一套难以证明完整的长期状态。


invocation-local reuse 为什么更适合这类项目

相比 persistent cache,我更喜欢单次 invocation 范围内的 reuse。

原因很简单。

它的生命周期非常清楚:

Plaintext
命令启动
→ 读取当前 exact bytes
→ 完成正式 currentness 检查
→ 在这一次 invocation 内复用确定性结果
→ 命令结束
→ reuse state 消失

下一次命令:

重新从 disk authority 开始。

这样不会出现:

昨天建立的缓存今天还认为自己是 current。

这和正式 workflow 的语义非常契合。


export 和 current-check 也不能为了快而变成同一次证明

还有一个非常重要的例子。

Generation Bundle 的:

export

和:

current-check

虽然都可能执行很多相同计算,但它们是两个独立 invocation。

current-check 的意义之一就是:

重新执行 canonical exporter,确认从 export 到现在输入没有变化。

如果为了性能把它改成:

export 时已经算过了,所以 check 直接复用。

那其实就把:

freshness proof

一起优化掉了。

所以正式 contract 明确要求:

两个 invocation 的 memo scope 必须分开。

性能测试也不能偷偷共享同一个 memo scope,然后得出:

current-check 很快。

因为这种 benchmark 并不对应真实正式流程。


benchmark 本身也可能“作弊”

性能测试还有一个很现实的问题。

如果 benchmark 使用:

  • warm cache;
  • 旧解析对象;
  • 上一次 invocation 的 current result;
  • 已经构建好的 graph;

那么数字可能非常漂亮。

但正式执行却不具备这些前提。

所以性能测试必须尽量回答:

这个测量真的代表用户会执行的那条命令吗?

而不是:

某个内部函数在理想缓存状态下有多快?

真实 fixture 很重要。

固定 targeted tests 也很重要。


为什么我没有继续追求 35 秒

当一条原本非常慢的 workflow 已经降到:

一两分钟左右的可接受范围。

继续往下优化时,收益性质已经变化了。

前面的收益是:

消除明显等待。

后面的收益越来越像:

为了漂亮数字继续压 benchmark。

如果这时候需要:

  • typed memo;
  • LRU;
  • 多级 cache;
  • persistent state;
  • architecture refactor;

那么它已经开始触碰另一条边界:

性能复杂度可能超过性能收益。

这时候停止并不是:

优化失败。

反而可能说明:

工程目标已经完成。


Codex 配额本身也是性能成本的一部分

还有一个以前不太会考虑的问题:

实现优化也消耗资源。

特别是在大量使用 Codex 做 repository engineering 时,一个复杂 cache 方案可能需要:

  • 设计;
  • 修改;
  • targeted tests;
  • failure diagnosis;
  • rollback;
  • 再实现;
  • 再验证。

这些都会消耗 5h quota。

如果剩余项目里还有:

  • correctness blocker;
  • Activation;
  • Preview;
  • Production;

那么继续花大量额度只为了:

再省几十秒。

机会成本就很高。

所以性能优化的性价比不能只看:

Plaintext
runtime saved

还应该看:

Plaintext
runtime saved
对比
implementation cost
对比
verification cost
对比
regression risk
对比
Codex quota

我后来给性能优化定了一个更明确的停止条件

现在我的思路更接近:

先问:

这是高频还是低频命令?

再问:

当前耗时是否已经影响实际工作?

再问:

profile 是否仍然存在明显大头?

再问:

下一项优化是不是简单、局部、容易证明?

如果答案逐渐变成:

低频;

已经可接受;

剩余瓶颈很分散;

需要复杂 cache 才能继续明显下降;

那就应该停。

也就是:

性能任务不需要把时间压到理论最低值。


撤销优化并不等于白做

这一轮让我感受很明显的一点是:

一项优化最后被 revert,不代表它完全没有价值。

因为它至少回答了:

这个方向能不能继续安全推进?

比如 35.93 秒这个结果证明:

性能上确实还有很大的理论空间。

但对应测试和证明又告诉我:

当前这个实现方式不值得保留。

这其实已经是一条很有价值的信息。

比起把有风险的优化硬留在仓库里,撤销以后还能保留:

  • profile;
  • benchmark;
  • failed test evidence;
  • 对 cache 边界的认识;
  • 更明确的性能政策。

这比一个“很快但说不清是否安全”的实现更有价值。


每项优化都应该能够单独撤回

后来我也越来越喜欢事务式推进。

不要一次同时做:

Plaintext
优化 A
→ 优化 B
→ 优化 C
→ 最后统一测试

因为如果最终失败,很难知道:

到底是哪一个改变破坏了 contract?

更好的方式是:

Plaintext
完成优化 A
→ 运行 targeted tests
→ PASS 后保留

完成优化 B
→ 运行 targeted tests
→ FAILED
→ 精确撤销 B 自己的改动

这样不会因为追求速度,让工作树里堆积一串未经证明的优化。


“优化后输出一样”仍然不一定足够

性能优化里还有一个容易忽略的问题。

即使 happy-path 输出 JSON:

Plaintext
before == after

也不一定足够。

因为还必须测试:

如果依赖在中间发生变化,新实现是否仍然会检测出来?

也就是说,既要证明:

正常输入结果没有变化。

还要证明:

应该失败的输入仍然失败。

这也是为什么 negative fixture 对性能优化特别重要。


correctness baseline 必须比 benchmark 更早建立

理想的优化顺序应该是:

Plaintext
先固定 correctness baseline
→ 再记录 before timing
→ profile 找瓶颈
→ 做一项修改
→ 跑 fixed targeted tests
→ PASS 后重新测 timing

而不是:

Plaintext
先改很多代码
→ 看到速度很快
→ 再想办法证明没问题

后者很容易产生心理偏差。

因为看到:

35 秒

以后,人会很自然地希望:

能不能调整一下测试把它保住?

这恰恰是最危险的方向。

正确顺序应该反过来:

测试 contract 不为优化让路。


更快,但测试 FAILED,就不能叫性能 PASS

所以我现在给性能任务的判断其实很简单。

例如:

Plaintext
执行时间:显著下降
targeted correctness tests:FAILED

最终结论仍然只能是:

Plaintext
RESULT=FAILED

不能写成:

性能优化成功,但还有少量测试需要处理。

因为如果测试覆盖的是正式 correctness invariant,那么这项优化就还没有成立。


这和整个翻译工作流其实是同一种思路

后来我发现,性能优化的原则和翻译 lifecycle 很像。

翻译里不能因为:

译文看起来不错。

就跳过 machine validation 和 Independent QC。

性能里也不能因为:

benchmark 看起来特别漂亮。

就跳过 correctness proof。

两者的共同原则其实都是:

不能拿结果表象替代正式证明。


从“最快”转向“高价值”

如果只追求最快,目标可能是:

Plaintext
找到所有 duplicate work
→ 全部消除
→ benchmark 尽量接近理论最小值

但我现在更愿意把目标写成:

Plaintext
找出高成本瓶颈
→ 使用简单方案减少明显重复
→ 保持 correctness 与 freshness
→ 达到实际可接受耗时
→ 停止

最终判断不是:

还能不能快。

而是:

下一秒性能收益是否仍然值得对应的工程成本和风险。


我现在更认可的几条原则

经过这几轮优化,我觉得可以把性能策略浓缩成几条规则。

第一:

先 profile,再优化;不要凭感觉猜瓶颈。

第二:

优先处理明显、高收益的重复计算,不追求清除所有 duplicate work。

第三:

优先使用 invocation-local reuse,谨慎引入 persistent cache、LRU 和复杂 memoization。

第四:

任何优化都不能降低 correctness、freshness、stale detection、identity、hash validation、dependency mutation detection 和 fail-closed。

第五:

export 和 current-check 之类本来就用于重新证明 freshness 的独立 invocation,不能因为性能优化而共享旧 PASS。

第六:

性能测试不仅要测 happy path,还必须保留 stale、mutation 和 invalid identity 等 negative fixtures。

第七:

一项优化一次提交思想上独立推进,targeted tests FAILED 就精确撤销这一项。

第八:

低频工作流已经从几分钟降低到一两分钟以后,继续节省几十秒通常不值得引入复杂缓存架构。

第九:

Codex 配额、实现时间、验证成本和未来维护负担,也属于性能优化成本。

第十:

性能任务真正的完成标准不是没有任何重复计算,而是没有剩余的高价值优化。

也就是:

Plaintext
REMAINING_HIGH_VALUE_OPTIMIZATION=none

最终我现在最愿意保留的一句话是:

最快的实现,不一定是最便宜的实现;而最便宜的实现,也不只是看运行时间。

对于这类强调 currentness、历史证据和可恢复性的工作流来说:

能稳定地从 10 分钟降到 2 分钟,通常比冒着失效检测风险追到 30 秒更有价值。

审核都通过了,为什么上线前还要再检查一次?v2 的 Activation、prior SHA 与 CAS 设计

Go 学习与文档多语言本地化项目

本系列完整记录 Go 学习与文档多语言本地化项目从 A Tour of Go 起步,逐步扩展到站点首页、项目说明页、学习内容、文档以及未来内容包的实际开发过程,包括架构设计、统一术语治理、整页翻译、结构保护、自动校验、独立质量审核、生产发布与长期维护。

项目源码:
✅ GitHub:shuijingwan/go-tour-i18n

语言版本:

当前跨项目目标语言池共包含 66 个语言版本。Go 项目本身目前提供 65 个社区 Production 语言版本,下面每一门 Go 语言版本都直接链接到其当前公开站点。语言列表沿用项目首页现有的展示顺序。

英语(美国)仅作为跨项目目标语言纳入列表,并不是单独的 Go Production 语言版本。当前 Go 的英文源内容仍为官方 generic English,因此英语(美国)链接到 Go 官方站点,而不是不存在的本地社区部署。

  1. 阿姆哈拉语
  2. 阿拉伯语
  3. 孟加拉语
  4. 葡萄牙语(巴西)
  5. 保加利亚语
  6. 加泰罗尼亚语
  7. 克罗地亚语
  8. 捷克语
  9. 丹麦语
  10. 荷兰语
  11. 英语(美国)(Go 官方英文站点)
  12. 英语(澳大利亚)
  13. 英语(加拿大)
  14. 英语(印度)
  15. 英语(新加坡)
  16. 英语(南非)
  17. 英语(英国)
  18. 爱沙尼亚语
  19. 菲律宾语
  20. 芬兰语
  21. 法语(法国)
  22. 法语(加拿大)
  23. 德语(德国)
  24. 德语(奥地利)
  25. 德语(瑞士)
  26. 希腊语
  27. 古吉拉特语
  28. 希伯来语
  29. 印地语
  30. 匈牙利语
  31. 印度尼西亚语
  32. 意大利语
  33. 日语
  34. 卡纳达语
  35. 哈萨克语
  36. 韩语
  37. 西班牙语(拉丁美洲)
  38. 拉脱维亚语
  39. 立陶宛语
  40. 马来语
  41. 马拉雅拉姆语
  42. 马拉地语
  43. 书面挪威语(Bokmål)
  44. 波斯语
  45. 波兰语
  46. 葡萄牙语(葡萄牙)
  47. 旁遮普语(果鲁穆奇文,印度)
  48. 罗马尼亚语
  49. 俄语
  50. 塞尔维亚语(西里尔文)
  51. 简体中文(中国大陆)
  52. 简体中文(新加坡)
  53. 斯洛伐克语
  54. 斯洛文尼亚语
  55. 西班牙语(西班牙)
  56. 斯瓦希里语(坦桑尼亚)
  57. 瑞典语
  58. 泰米尔语
  59. 泰卢固语
  60. 泰语
  61. 繁体中文(台湾)
  62. 繁体中文(香港)
  63. 土耳其语
  64. 乌克兰语
  65. 乌尔都语
  66. 越南语

项目正在持续扩展新的 Go 学习与文档内容,并长期维护各语言版本的术语一致性、翻译质量、独立审核、生产发布与后续更新。

本项目属于非官方社区多语言本地化项目,与 Go 官方无隶属、授权或背书关系。