深入 Go 编译器 SSA 后端:Value、Memory、Block、优化 Pass 与重写规则全解(go/go)
本文基于 Go 源码仓库中 src/cmd/compile/internal/ssa 包的官方文档 src/cmd/compile/internal/ssa/README.md 展开,系统讲解 Go 编译器(gc)SSA 后端的三大核心概念——Value、Memory 类型与 Block,并结合仓库源码梳理完整的优化 pass 流水线(passes 列表)、lower 降低 pass 的特殊角色、GOSSAFUNC 调试技巧,以及用 .rules 重写规则和 *Ops.go 表扩展 SSA 的方法。读完后,你可以读懂 ssa.html 中每一列 pass 输出的含义,并能独立追踪一个 Go 函数从 SSA 形式到机器相关代码的完整变换链。
SSA 在 Go 编译器中的位置
要理解 SSA 后端的意义,先看整个 cmd/compile 编译流水线。根据编译器总览文档 src/cmd/compile/README.md,编译逻辑上分为若干阶段:
- 解析:
cmd/compile/internal/syntax(词法、语法分析、语法树); - 类型检查:
cmd/compile/internal/types2; - IR 构建(noding):
cmd/compile/internal/ir、cmd/compile/internal/noder,把语法树转换成编译器自己的 AST 表示; - 中端:内联(
inline)、去虚化(devirtualize)、逃逸分析(escape); - Walk:
cmd/compile/internal/walk,把复杂语句拆成简单语句、按求值顺序引入临时变量,并做语法脱糖(如switch转跳转表、map/channel 操作替换为 runtime 调用); - 通用 SSA:
cmd/compile/internal/ssa与cmd/compile/internal/ssagen,把 IR 转成静态单赋值(Static Single Assignment)形式,随后运行一系列机器无关的 pass 与重写规则; - 机器代码生成:从
lowerpass 开始进入机器相关阶段,最终 SSA 被转换为obj.Prog指令序列交给汇编器cmd/internal/obj输出目标文件。
ssa 包正是第 6、7 两阶段的核心。它把程序转成 SSA 形式——一种让优化实现变得容易的低级中间表示(IR)——然后通过一系列 pass 逐步优化、降低,最终逼近可汇编的形态。需要注意的是:ssa 包里的概念(Value、Block 等)虽然与 Go 语言概念名称相近,但并不等价。例如 Go 的块语句有变量作用域,而 SSA 中根本没有“变量”与“变量作用域”的概念。
核心概念一:Value
Value 是 SSA 的基本构建块。按照 SSA 的定义,一个 Value 恰好被定义一次,但可以被使用任意多次。一个 Value 主要由四部分组成:唯一标识符(ID)、操作符(Op)、类型,以及一些参数。
- Op(操作符):描述计算该 Value 的操作。每个操作符的语义定义在
src/cmd/compile/internal/ssa/_gen/目录下的*Ops.go文件中(如 genericOps.go)。例如OpAdd8接收两个存放 8 位整数的 Value 参数,结果就是二者的加法。 - 类型:通常是某个 Go 类型,比如
uint8、bool;但也有一些不来自 Go 的特殊类型,最常见的是memory(下文详述)。 - 辅助字段(aux):某些操作符带有 aux 字段,打印时通常包在
[]或{}里,可以是常量操作数、参数类型等。 - 顺序编号:Value 和 Block 都以唯一的顺序 ID 命名(如
v4、b2),它们很少直接对应源码中的变量或参数名。顺序 ID 的好处是:编译器可以避免使用 map(Value 数组按下标直接寻址);同时借助调试信息(position info)始终可以把 Value 追溯回原始 Go 代码。
一个 uint8 加法在 SSA 中的表示(源自 README 中的示例):
// var c uint8 = a + b
v4 = Add8 <uint8> v2 v3
aux 字段的两个例子:
v13 (?) = Const64 <int> [1]
这里 aux 字段是常量操作数——该 Op 创建了一个值为 1 的 Const64。
v17 (361) = Store <mem> {int} v16 v14 v8
这里 aux 字段是被存储值的类型 int。
更多 Value 的定义可参考 value.go 与 src/cmd/compile/internal/ssa/_gen/ 中的 *Ops.go 文件。
核心概念二:Memory 类型——用数据流表示内存状态
memory 类型表示全局内存状态。在 SSA 中一切皆为“值”(定义一次、可多次使用),但真实的内存读写会破坏这个性质:读的结果依赖之前所有写。Go 编译器 SSA 的解法是把内存本身也建模成一个值:
- 接收
memory参数的 Op,表示它依赖某个内存状态; - 产生
memory类型结果的 Op,表示它改变了内存状态。
这样,内存操作就能通过值的依赖链保持正确的先后顺序。以 README 中的例子为例:
// *a = 3
// *b = *a
对应的 SSA:
v10 = Store <mem> {int} v6 v8 v1
v14 = Store <mem> {int} v7 v8 v10
这里每个 Store 把第二个参数(类型 int,即要存的值)写入第一个参数(类型 *int,即目标地址);最后一个参数是输入内存状态。第二个 Store 的内存状态参数是 v10——即第一个 Store 的输出,因此第二个 Store 依赖第一个 Store 定义的内存状态,两者不能重排序。这条“store 链”是理解 SSA 中所有内存相关 pass(如死存储消除 dse、memcombine)的钥匙。
核心概念三:Block(基本块)
Block 表示函数控制流图(CFG)中的一个基本块,本质上是“定义该块行为的一组 Value 的列表”。除 Value 列表外,Block 主要由唯一标识符、kind(种类)以及后继块列表组成。三种最重要的 kind:
- plain 块:把控制流交给另一个块,后继列表恰好一个元素;
- exit 块:带有一个最终的控制值(control value),且该值必须返回一个 memory 状态。函数要能返回结果,调用方必须依赖某个内存状态才能正确读到返回值,所以 exit 块必须吐出内存状态;
- if 块:有一个布尔控制值,恰好两个后继块——控制值为 true 时把控制流交给第一个后继,否则交给第二个。
下面是一个完整的 if-else 控制流示例(README 原样示例,注释给出了对应的 Go 源码):
// func(b bool) int {
// if b {
// return 2
// }
// return 3
// }
b1:
v1 = InitMem <mem>
v2 = SP <uintptr>
v5 = Addr <*int> {~r1} v2
v6 = Arg <bool> {b}
v8 = Const64 <int> [2]
v12 = Const64 <int> [3]
If v6 -> b2 b3
b2: <- b1
v10 = VarDef <mem> {~r1} v1
v11 = Store <mem> {int} v5 v8 v10
Ret v11
b3: <- b1
v14 = VarDef <mem> {~r1} v1
v15 = Store <mem> {int} v5 v12 v14
Ret v15
值得注意的细节:返回值并非直接“return”,而是先把结果 Store 到栈上的一个地址 v5(Addr <*int> {~r1} v2,即从栈指针 SP 推出返回值的存储位置),再由 Ret 指令带上新的内存状态结束。这正体现了“一切皆 Value”的 SSA 思想——函数返回值也是一次显式的内存写。更多 Block 定义见 block.go。
核心概念四:Function
SSA 中的 Function 对应一个函数声明及其函数体,主要由名称、类型(签名)、组成函数体的 Block 列表,以及其中的**入口块(entry block)**构成:
- 函数被调用时,控制流交给其入口块;函数结束时控制流会到达某个 exit 块,从而结束调用;
- 一个函数可以有零个或多个 exit 块(Go 函数可以有任意多个 return 点),但入口块必须恰好一个;
- 部分 SSA 函数是自动生成的,例如每种用作 map key 的类型都会生成对应的哈希函数。
一个空函数在 SSA 中的形态(README 原样示例):
foo func()
b1:
v1 = InitMem <mem>
Ret v1
它只有一个 exit 块,返回一个“无关紧要”的内存状态。更多细节见 func.go。
优化 Pass 流水线:passes 列表与顺序约束
程序被表示成 SSA 之后,价值才真正体现出来:SSA 让“修改程序使其更好”的优化更容易编写。Go 编译器通过一组顺序执行的 pass 完成这件事——每个 pass 以某种方式变换一个 SSA 函数。例如死代码消除 pass 会删除可证明永远不会执行的块与值;nil 检查消除 pass 会删除冗余的 nil 检查。pass 每次只处理一个函数,默认顺序执行、各跑一次。
Pass 的结构定义
pass 的数据结构定义在 src/cmd/compile/internal/ssa/compile.go:
type Pass struct {
Name string
Fn func(*Func)
Required bool
Disabled bool
Time bool // report time to run pass
Mem bool // report mem stats to run pass
Stats int // pass reports own "stats" (e.g., branches removed)
Debug int // pass performs some debugging.
Test int // pass-specific ad-hoc option
Dump map[string]bool // dump if function name matches
Keywords map[string]int64 // ad hoc parameters, typically for experiments/tuning
UsedKW map[string]bool
}
Required 标记的 pass 不可用 -d=ssa/... 调试选项禁用;Dump 与 Keywords 则支撑了 GOSSAFUNC 里“指定 pass 打印”与“调参实验”的能力。
完整的 passes 列表
pass 列表定义在 src/cmd/compile/internal/ssacompile/compile.go,这是理解整个 SSA 后端的地图。完整流水线如下(注释为源码原注释):
var passes = [...]ssa.Pass{
{Name: "number lines", Fn: numberLines, Required: true},
{Name: "early phielim and copyelim", Fn: copyelim},
{Name: "early deadcode", Fn: deadcode}, // remove generated dead code to avoid doing pointless work during opt
{Name: "short circuit", Fn: shortcircuit},
{Name: "decompose user", Fn: decomposeUser, Required: true},
{Name: "pre-opt deadcode", Fn: deadcode},
{Name: "opt", Fn: opt, Required: true},
{Name: "zero arg cse", Fn: zcse, Required: true}, // required to merge OpSB values
{Name: "opt deadcode", Fn: deadcode, Required: true}, // remove any blocks orphaned during opt
{Name: "generic cse", Fn: cse},
{Name: "phiopt", Fn: phiopt},
{Name: "gcse deadcode", Fn: deadcode, Required: true}, // clean out after cse and phiopt
{Name: "nilcheckelim", Fn: nilcheckelim},
{Name: "prove", Fn: prove},
{Name: "divisible", Fn: divisiblePass, Required: true},
{Name: "divmod", Fn: divmodPass, Required: true},
{Name: "middle opt", Fn: opt, Required: true},
{Name: "known bits", Fn: ssa.KnownBits},
{Name: "early fuse", Fn: fuseEarly},
{Name: "expand calls", Fn: expandCalls, Required: true},
{Name: "decompose builtin", Fn: postExpandCallsDecompose, Required: true},
{Name: "softfloat", Fn: softfloat, Required: true},
{Name: "branchelim", Fn: branchelim},
{Name: "late opt", Fn: opt, Required: true},
{Name: "dead auto elim", Fn: elimDeadAutosGeneric},
{Name: "sccp", Fn: sccp},
{Name: "generic deadcode", Fn: deadcode, Required: true}, // remove dead stores, which otherwise mess up store chain
{Name: "late fuse", Fn: fuseLate},
{Name: "check bce", Fn: checkbce},
{Name: "dse", Fn: dse},
{Name: "memcombine", Fn: memcombine},
{Name: "writebarrier", Fn: writebarrier, Required: true}, // expand write barrier ops
{Name: "insert resched checks", Fn: insertLoopReschedChecks,
Disabled: !buildcfg.Experiment.PreemptibleLoops},
{Name: "cpufeatures", Fn: cpufeatures, Required: buildcfg.Experiment.SIMD, Disabled: !buildcfg.Experiment.SIMD},
{Name: "rewrite tern", Fn: rewriteTern, Required: false, Disabled: !buildcfg.Experiment.SIMD},
{Name: "lower", Fn: lower, Required: true},
{Name: "addressing modes", Fn: addressingModes, Required: false},
{Name: "late lower", Fn: lateLower, Required: true},
{Name: "pair", Fn: pair},
{Name: "lowered deadcode for cse", Fn: deadcode},
{Name: "lowered cse", Fn: cse},
{Name: "elim unread autos", Fn: elimUnreadAutos},
{Name: "tighten tuple selectors", Fn: tightenTupleSelectors, Required: true},
{Name: "lowered deadcode", Fn: deadcode, Required: true},
{Name: "checkLower", Fn: checkLower, Required: true},
{Name: "loop invariant", Fn: licm},
{Name: "late phielim and copyelim", Fn: copyelim},
{Name: "tighten", Fn: tighten, Required: true}, // move values closer to their uses
{Name: "late deadcode", Fn: deadcode},
{Name: "critical", Fn: critical, Required: true}, // remove critical edges
{Name: "phi tighten", Fn: phiTighten},
{Name: "likelyadjust", Fn: likelyadjust},
{Name: "layout", Fn: layout, Required: true}, // schedule blocks
{Name: "schedule", Fn: schedule, Required: true}, // schedule values
{Name: "late nilcheck", Fn: nilcheckelim2},
{Name: "flagalloc", Fn: flagalloc, Required: true}, // allocate flags register
{Name: "regalloc", Fn: regalloc, Required: true}, // allocate int & float registers + stack slots
{Name: "loop rotate", Fn: loopRotate},
{Name: "trim", Fn: trim}, // remove empty blocks
}
从这份列表可以读出流水线的大致分段:
- 通用优化段(机器无关):从
number lines到check bce,反复出现opt(重写规则应用)、deadcode、cse。注意 deadcode 被调用了很多次(early / pre-opt / opt / gcse / generic / late / lowered…),因为几乎每个会引入或暴露新死代码的 pass 之后都要清理一次; - 内存相关段:
dse(死存储消除)依赖 CSE 识别“两个地址表达式相同”,memcombine合并相邻的内存操作,writebarrier展开写屏障; - 机器相关段:以
lower为分界线,之后还有addressing modes、late lower、pair(成对指令组合)、tighten(把值挪近使用点)、layout/schedule(块与指令调度)、regalloc(寄存器分配)等; - 实验开关:
cpufeatures、rewrite tern仅在 SIMD 实验开启时启用,insert resched checks依赖可抢占循环实验。
顺序约束(passOrder)
为什么 pass 的顺序很重要?同一文件下方(src/cmd/compile/internal/ssacompile/compile.go)用一张 passOrder 约束表显式记录了关键顺序要求:
// Double-check phase ordering constraints.
var passOrder = [...]constraint{
// "insert resched checks" uses mem, better to clean out stores first.
{"dse", "insert resched checks"},
// insert resched checks adds new blocks containing generic instructions
{"insert resched checks", "lower"},
{"insert resched checks", "tighten"},
// prove relies on common-subexpression elimination for maximum benefits.
{"generic cse", "prove"},
// deadcode after prove to eliminate all new dead blocks.
{"prove", "generic deadcode"},
// common-subexpression before dead-store elim, so that we recognize
// when two address expressions are the same.
{"generic cse", "dse"},
// cse substantially improves nilcheckelim efficacy
{"generic cse", "nilcheckelim"},
// nilcheckelim relies on the first opt to rewrite user nil checks
{"opt", "nilcheckelim"},
// tighten will be most effective when as many values have been removed as possible
{"generic deadcode", "tighten"},
...
}
这解释了 README 中“pass 顺序很重要”的原因:比如 prove(谓词推理)要先有 generic cse 才最有收益;dse 必须先做 CSE 才能认出相同地址;tighten 要在尽可能多的值被删除之后才最有效。同一优化多次出现(如 deadcode、opt、cse、phielim/copyelim)也由此得到解释——不同阶段清理的目标不同。
lower pass 的特殊性
所有 pass 中 lower 是特殊的:它把 SSA 从机器无关表示转换为机器相关表示——一些抽象操作符被替换成机器相关的非通用版本,最终 Value 的数量可能减少也可能增加。以 amd64 为例(见编译器总览文档):amd64 支持内存操作数,因此许多 load-store 操作可以合并。并且 lower 会运行所有机器相关重写规则(如 AMD64.rules),因此它实际上也承担了大量优化工作。
动手观察 SSA:GOSSAFUNC 实战
最直观的上手方式是用 GOSSAFUNC 环境变量观察 SSA:
基本用法
GOSSAFUNC=Foo go build
会针对名为 Foo 的函数生成 ssa.html。该文件包含该函数在每个编译 pass 阶段的 SSA 快照,以多列形式并排展示,可以直观看到每个 pass 对程序做了什么;页面上还可以点击 Value 和 Block 高亮跟踪控制流与数据流。
包限定的函数名
GOSSAFUNC=blah.Foo go build
这会匹配任何包名最后一段为 blah 的包中名为 Foo 的函数(例如 something/blah.Foo、anotherthing/extra/blah.Foo 都会命中)。
打印指定 pass 的 CFG
GOSSAFUNC="blah.Foo:sccp,generic deadcode" go build
格式为 $FunctionName:$PassName1,$PassName2,...,会在 sccp 与 generic deadcode 两列旁边附加打印控制流图(CFG),便于分析这两个阶段前后的控制流差异。
输出到 stdout
如果不需要 HTML,在值后加 + 号,dump 直接写到标准输出:
GOSSAFUNC=Bar+ go build
GOSSADIR 与 per-pass 文件 dump
从源码看,pass 还支持把每个阶段的结果写成独立文件:src/cmd/compile/internal/ssa/compile.go 中 DumpFileForPhase 会生成 <函数名>_<序号>__<阶段名>.dump 文件,若设置了 GOSSADIR 环境变量则写入该目录。对应 Pass 结构中的 Dump map[string]bool 字段,即“函数名匹配时 dump 该阶段”。
扩展 SSA:重写规则(.rules)与 Op 表
大多数编译器 pass 用 Go 代码直接实现,但另一部分优化是代码生成出来的,其源头是 src/cmd/compile/internal/ssa/_gen/ 目录下的 .rules 文件。这些文件有自己的语法,由 rulegen.go 解析生成 Go 代码。简单的优化用重写规则可以快速轻松地表达;但复杂优化不适合用规则表达,需要写专门的 pass。
generic.rules 头部注释给出了规则语法的核心约定:
// values are specified using the following format:
// (op <type> [auxint] {aux} arg0 arg1 ...)
// the type, aux, and auxint fields are optional
// on the matching side
// - the type, aux, and auxint fields must match if they are specified.
// - the first occurrence of a variable defines that variable.
// Subsequent uses must match (be == to) the first use.
// - v is defined to be the value matched.
// - an additional conditional can be provided after the match pattern with "&&".
// on the generated side
// - the type of the top-level expression is the same as the one on the left-hand side.
// - the type of any subexpressions must be specified explicitly.
// - auxint will be 0 if not specified.
// - aux will be nil if not specified.
直观的例子就是文件开头的常量折叠规则:
// constant folding
(Trunc16to8 (Const16 [c])) => (Const8 [int8(c)])
(Trunc32to8 (Const32 [c])) => (Const8 [int8(c)])
(Cvt32Fto32 (Const32F [c])) && c >= -1<<31 && c < 1<<31 => (Const32 [int32(c)])
规则左侧是匹配模式(type/aux/auxint 在匹配侧是可选的、变量首次出现即定义),右侧是替换生成侧(子表达式类型必须显式给出),还可以用 && 追加布尔条件。文件顶部注释也举了经典例子:y := 0 * x 可以被规则安全地化简为 y := 0,省去一次无谓的乘法指令。
_gen/ 目录下按架构组织了规则与 Op 定义:
| 文件/目录 | 作用 |
|---|---|
| generic.rules / genericOps.go | 所有架构通用的重写规则与通用操作符定义 |
| AMD64.rules / AMD64Ops.go | amd64 架构相关规则与操作符 |
| ARM64.rules、RISCV64.rules、Wasm.rules 等 | 其他 GOARCH 的规则文件 |
| simd*.rules 系列 | SIMD 实验相关的规则 |
| main.go / rulegen.go | 规则解析器与代码生成器(go generate 的入口) |
同样,管理操作符(Op)本身的代码也是从 _gen/*Ops.go 生成的——维护少数表格比维护大量重复代码更容易。修改规则或操作符后,运行:
go generate cmd/compile/internal/ssa
即可重新生成 Go 代码。
关键源码索引
按阅读顺序,理解 SSA 后端的仓库路径:
- src/cmd/compile/internal/ssa/README.md:本文的基础文档(SSA 后端介绍);
- src/cmd/compile/README.md:编译器整体阶段划分,理解 SSA 所处位置;
- src/cmd/compile/internal/ssa/value.go、block.go、func.go、op.go:Value、Block、Function、Op 的核心定义;
- src/cmd/compile/internal/ssa/compile.go:
Compiler接口、Pass结构、per-phase dump 实现; - src/cmd/compile/internal/ssacompile/compile.go:完整
passes列表与passOrder顺序约束,是 pass 流水线的一手资料; - src/cmd/compile/internal/ssa/_gen/generic.rules、rulegen.go:重写规则语法与代码生成器;
- src/cmd/compile/internal/ssa/deadcode.go、cse.go、rewrite.go、regalloc.go:各 pass 的具体实现,对照 passes 列表逐一阅读即可还原整条优化链。
需要强调的是适用前提:以上 pass 列表、规则文件均以当前仓库源码为准,实验特性(如 buildcfg.Experiment.SIMD、PreemptibleLoops)的默认开关可能随版本变化;阅读具体行为时,请结合 src/cmd/compile/internal/ssacompile/compile.go 中的 Disabled 字段与 goexperiment 包确认实际启用状态。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00