15.7 过去、现在与未来
编译器是 Go 工具链里改动最频繁、却对用户最透明的部分。同一份源码一行不改,换一个新版本重新 编译,往往就更快、更小、更优,你甚至不知道中间发生了什么。这一节把镜头拉远,回看编译器自身 走过的路,再看它当下在做什么、接下来会往哪里走。贯穿始终的,是一条不变的价值排序:编译速度 优先,生成代码质量其次,而两者都要在「可工程化」的前提下取舍 (1.1)。
值得先说清楚的是,本节谈的「变」全部发生在引擎盖之下。Go 对语言与工具的兼容性承诺,意味着这些 重构不该惊动任何一行用户代码。下面要讲的两次大重写,正是在这个约束下完成的。
15.7.1 过去:从 C 到 Go,从 Plan 9 到 SSA
编译器自身经历了两次伤筋动骨的大变,方向却截然不同:一次换的是实现语言,一次换的是后端架构。
其一,实现语言:C → Go。 最早(到 Go 1.4 为止)的 gc 编译器是用 C 写的,沿用了 Plan 9
工具链的代码风格与构建方式。Go 1.5 完成了自举(bootstrap):编译器被机器翻译成 Go,从此
是「用 Go 写的 Go 编译器」。这件事的细节见 3.3,
这里只强调它的意义。换语言不是为了赶时髦:C 版本无法利用 Go 自己的并发、内存安全与丰富的标准
库,也无法让 Go 社区用熟悉的语言去读、去改编译器。自举之后,编译器才真正成为「社区可维护的
Go 程序」,这为后续一切重构铺平了路。代价是引入了一个自举链:要编译当前版本的 Go,先得有
一个能跑的旧版本 Go,工具链因此需要小心维护这条向后的依赖。
顺带澄清一个常被混淆的命名:编译器叫 gc,是 “Go compiler” 的缩写,与垃圾回收(大写 GC,
garbage collection)毫无关系。cmd/compile/README 开篇就专门提醒了这一点。
其二,后端架构:Plan 9 风格 → SSA。 自举只是换了笔,没换骨架。Go 1.5/1.6 的后端仍大体 沿用 Plan 9 编译器的传统设计:基于一种较低层的指令表示直接做有限的优化与指派。这套后端能用, 但难以承载现代优化,它的中间表示不便于做数据流分析,加一个新优化往往要在多处特判,扩展性差。
Go 1.7 引入了 SSA 后端(15.2),这是编译器历史上最关键的一次架构升级。SSA (Static Single Assignment,静态单赋值)要求每个变量只被赋值一次,于是「这个值从哪来、到哪去」 变成显式的图结构,死代码消除、常量传播、冗余消除、边界检查消除等优化都能写成对图的重写 规则,彼此独立、可组合、可逐架构定制。一个直观的例子是边界检查消除:
| |
这类「规则」在 SSA 后端里有非常具体的形态。Go 的机器无关与机器相关优化大量写成形如
(原模式) -> (替换) 的重写条目,例如把一条已知右操作数为 2 的幂的乘法改写为移位:
| |
编译器在构建时把成百上千条这样的规则编译进优化器,逐趟扫过 SSA 图反复套用,直到不再有规则可 触发。新增一项优化,多数时候就是新增几条规则,而不必改动框架本身,这正是 SSA 后端「可扩展」 的含义所在。
在 Plan 9 风格的后端里,要稳定地做到边界检查消除这类证明并不容易;在 SSA 框架下,它只是「在 值的关系图上跑几条规则」。这次重构同样对用户透明:人们没改一行代码,只是发现新版本编译出的程序更快了、 二进制更小了。SSA 后端最初只在 amd64 上启用,随后逐架构铺开,至今仍是 Go 代码生成的主干, 也是后面 PGO、寄存器 ABI 这些工作得以落地的舞台。
把这两次重写放进同行的坐标里看,会更清楚 Go 的取舍。多数主流语言的工业级编译器(如以 C++ 写成的 LLVM、GCC)从一开始就以 SSA 或类 SSA 的中间表示为核心,把代码生成质量放在很高的位置, 代价是编译速度与实现复杂度。Go 反其道而行:先用一个简单、极快的 Plan 9 风格后端把语言与生态 立起来,等社区与需求成熟,再补上 SSA 这块「代码质量」的短板。换句话说,别家是「先求好,再调 快」,Go 是「先求快,再补好」,这正是它「编译速度优先」价值排序的历史投影。
两次大变合起来看,传递的是同一种工程姿态:当旧地基撑不起未来时,敢于推倒重写,但每一次 重写都谨守对用户的透明承诺。 一条粗线条的时间轴:
| |
15.7.2 现在:寄存器 ABI 与 PGO
如果说过去是「换地基」,现在的两项进展则是在新地基上盖出的两座楼。它们共同的母题,是把编译器 从「静态地猜」推向「依据真实信息决策」。
寄存器调用约定(Go 1.17)。 长期以来,Go 的函数调用通过栈传递参数与返回值:调用方把 实参写进栈上约定的位置,被调方再从栈上读出。这套约定简单、跨架构一致、对栈扫描友好,但每次 调用都要做一串内存读写,开销不小。Go 1.17 把它改成了寄存器传参 (2.2、6.1): 前若干个参数与返回值直接走寄存器,命中的快路径完全不碰内存。
| |
整体提速约 5%,且对用户、对汇编以外的代码都透明,设计上特意保留了栈上的备份槽,使栈扫描、
recover、go:nosplit 等机制无需改写。这条改动的工程难点不在「想到用寄存器」(这是常识),
而在「在一个有 GC、有抢占、有跨架构汇编的运行时里,把寄存器传参做得不破坏任何既有不变量」。
它的设计文档(proposal 40724)值得一读,是「兼容约束下做底层优化」的范本。
性能制导优化 PGO(Go 1.21)。 编译器做内联、去虚化这些决策时,传统上只能静态地猜: 函数小就内联,类型能在编译期定死就去虚化。但「哪条路径是热点」这种信息,静态分析根本拿不到。 PGO(Profile-Guided Optimization)的思路是把真实运行的画像(profile)喂回编译器 (15.3):
| |
机制上,编译器从画像里读出一张带权的调用图:每条调用边有一个采样得到的热度。内联器据此放宽 对热边的预算,平时一个函数体超过预算就不内联,但若它处在被频繁走到的热路径上,就值得为它 破例展开;去虚化也类似,热点处某个接口调用若画像显示绝大多数走向同一具体类型,便可生成一条 直奔该类型的快路径,仅在类型不符时回退。换言之,PGO 不发明新的优化,它只是给既有优化一把 更准的尺子,让有限的「代码膨胀预算」花在真正值钱的地方。实测常带来个位数百分比的提升。PGO 的关键意义不在那几个百分点,而在它确立了一条新范式:编译器的优化决策从此可以由数据驱动, 而非只靠静态启发式猜测。
泛型支持的持续打磨。 与此并行的还有泛型。前端在 Go 1.18 换上了 types2 类型检查器
(8.3),后端采用 GC 形状 stenciling:按内存
布局分组生成代码,布局相同的一组类型共用一份机器码,类型相关信息经一个运行时字典在调用时
传入。这条路避免了对每个类型都单态化的代码爆炸,代价是引入了一层字典间接
(8.4)。削减这层间接的开销,是编译器至今仍在做的
功课,也正好接上下一节要谈的未来。
15.7.3 未来:在速度与优化之间继续走钢丝
编译器的未来,仍是那条贯穿全书的张力:编译速度与生成代码质量之间的平衡 (1.1)。Go 在这道题上的答案一向鲜明,而几条可 预见的方向,都是在这个答案的约束下展开的。
PGO 的进一步发力。 内联与去虚化只是 PGO 能利用画像的第一批场景。栈分配与逃逸的画像引导、 基本块布局(把热路径排在一起以改善取指与分支预测)、依据画像的特化,都是自然的延伸。难点在于 保持「无画像也能很快编译、有画像才换取更优代码」的可选性,PGO 永远不该成为编译的前置负担。
泛型代码的性能。 GC 形状字典那一层间接,是泛型当前最主要的性能税。把热点处的字典访问内联、 对单一实例化做更彻底的去虚化与特化,让泛型代码逐步逼近手写单态版本的速度,是一条没有「完成」 之日、却会持续推进的战线(8.4)。
与新 GC 协同的代码生成。 代码生成从不孤立。新一代 GC(Green Tea, 13.11)改变了扫描与写屏障的形态,编译器插入屏障、安排 指针存活信息(liveness)的方式也要随之演化,两者协同进化,才能把 GC 的吞吐改进真正落到生成 代码上。
新硬件的利用。 向量指令(SIMD)、更宽的寄存器、新的原子与内存序原语,这些硬件能力如何在 不破坏 Go 简单心智模型的前提下被编译器自动用上,是一个长期开放的题目。Go 倾向于在标准库与 运行时里以受控方式引入,而非把复杂度抛给用户。
但有一条几乎可以肯定不会变:Go 不会为了多榨几个百分点的运行性能,而牺牲它引以为傲的编译 速度。 快速编译是 Go 生产力叙事的立身之本,任何优化若显著拖慢编译,多半会被挡在门外,或 退化成可选项(如 PGO)。这条不变量,本身就是这台机器最重要的设计参数。
回看编译器这条线,它完美诠释了 Go 工具链的工作方式:持续地、对用户透明地变好;每一步都在 既定的价值排序(快、简单、可工程化)下做取舍;敢于推倒重写(C→Go、Plan 9→SSA),也敢于 引入新范式(PGO 的数据驱动)。 这台把源码变成飞快二进制的机器,本身就是 Go 工程哲学最精密 的体现。
延伸阅读的文献
- The Go Authors. cmd/compile/README:Introduction to the Go compiler. 编译器四阶段架构、
gc命名、types2 与 Unified IR 的演进。 https://github.com/golang/go/blob/master/src/cmd/compile/README - Keith Randall. Generating Better Machine Code with SSA. GopherCon / Go 1.7 SSA 后端的 设计动机与重写规则框架。https://go.dev/talks/2015/gogo.slide
- The Go Authors. Profile-guided optimization(PGO 用户文档). 采集画像、
default.pgo约定与当前能利用画像的优化。https://go.dev/doc/pgo - Austin Clements, Cherry Mui, et al. Proposal 40724:Register-based Go calling convention. 栈 ABI → 寄存器 ABI 的兼容性约束与设计。 https://go.googlesource.com/proposal/+/master/design/40724-register-calling
- The Go Authors. Go 1.5 / 1.7 / 1.17 / 1.21 Release Notes. 自举、SSA 后端、寄存器 ABI、 PGO 的逐版本落地记录。https://go.dev/doc/devel/release
- 本书 15.2 中间表示与 SSA、15.3 优化器与 PGO。
- 本书 8 泛型(8.3 types2、8.4 字典间接), 3.3 自举。
许可
© 2018-2026 The golang.design Initiative Authors. Licensed under CC-BY-NC-ND 4.0.