Go: compile 编译器
Outlinks (0)
No outlinks found
1 · Go 编译器#
1.1 · 如何使用 dlv 调试 complier#
为什么需要构建本地go和设置GOROOT环境变量?
让「用来编译的 go 命令」和「被编译的 cmd/compile」共用同一套内部包,避免混用不同版本的包导致编译失败或行为异常
# 1. 下载go源码,进入src目录,构建工具链后回到上层目录
./make.bash && cd ..
# 2. 设置GOROOT,指向本地源码根目录
export GOROOT=$(pwd)
cd $GOROOT/src
# 3. 编译(禁止优化)
$GOROOT/bin/go build -a -gcflags="all=-N -l" -o /tmp/compile cmd/compile
dlv exec /tmp/compile -- test.go
# 4. 使用dlv操作
# b gc.Main
1.2 · gopls setup#
# https://github.com/golang/tools/blob/master/gopls/doc/advanced.md#working-on-the-go-source-distribution
#
# 1. 先执行 ./src/make.bash 生成 bin/go
./make.bash && cd ..
# 2. 把自己构建的 go 放在 PATH 最前面,优先使用本地构建的 go
export PATH=/home/liu/dev/go_src/bin:$PATH
# 3. 设置GOROOT,指向本地源码根目录
export GOROOT=$(pwd)
# 4. 在 GOROOT/src 下创建 go.work: 把 std 和 cmd 都加入工作区
go work init . cmd
# 5. GOROOT/src 作为工作区
1.3 · 快速概览:编译流程
https://github.com/golang/go/blob/master/src/cmd/compile/README.md
Go 编译器将源代码转换为可执行文件,共8个主要阶段(官方分为前端、中端、后端):
源代码 → 词法/语法分析 → 类型检查 → IR生成 → 中端优化 → Walk → SSA → 机器码 → 可执行文件
└────── 前端 ──────┘ └─── 中端 ───────────┘ └──────── 后端 ─────┘
关键阶段:
- 词法/语法分析:源代码 → Token 流;Token → 抽象语法树AST(syntax.File)
- 类型检查:AST → 符号表、类型信息(使用 types2 包)
- 中间代码生成(Noding + typecheck):通过 Unified IR 将 语法树(syntax)和类型信息(types2) 转换为编译器的 IR(完成类型传播和隐式转换)
- 中端优化:内联、逃逸分析、死代码消除、去虚化等
- Walk 阶段:保证求值顺序、反糖、拆解复杂语句、转换高级构造
- SSA 生成与优化:IR → SSA → 机器无关优化
- 机器码生成:SSA 降级 → 机器相关优化 → 汇编 → 目标文件(→ 链接 → 可执行文件 这两步属于 link 阶段)
- Export:生成 export data,供下游包编译使用
IR vs. AST: IR也是树形结构,IR根据AST+type信息生成
IR vs. SSA: IR的优化是语言层面,SSA的优化是指令/值层面
NOTE: 类型检查分散在两处:
-
- types2:语义类型检查、接口实现、符号解析等
-
- typecheck:类型传播、插入隐式转换(如 CONVIFACE)等
1.4 · go tool compile#
go tool compile --help
# -N -l 禁用优化,禁用内联
# 不同参数,打印不同阶段的信息
# 1/2 阶段无
#
# walk 前后ir tree的区别 W是函数级/w是expr级
# -W debug parse tree after type checking
# -w debug type checking
# 中端优化
# -m print optimization decisions
# 大多数中端的操作,也有部分SSA/指令平台相关的操作
# -d value
# enable debugging settings; try -d help
# SSA相关操作
# -d=ssa/help
# 汇编
# -S
1.5 · 不同阶段的函数入口
b cmd/compile/internal/syntax.Parse
b types2.Config.Check
b noder.unified
# ir树按函数划分;后续优化也是以函数为基本单位
b noder.readBodies
# ir 类型检查,以节点为单位
# typecheck.Stmt / typecheck.Expr
b typecheck.typecheck
b DevirtualizeAndInlinePackage
b escape.Funcs
b deadlocals.Funcs
b walk.Walk
b ssagen.Compile
b ssa.Compile
b obj.Flushplist
gc.Main() // gc/main.go:61
──── 1. 解析 ────
noder.LoadPackage(filenames) // gc/main.go:208
├── syntax.Parse(...) // noder/noder.go:59
│ 词法分析 + 语法分析 → syntax.File
│
──── 2. types2 类型检查 + 3. IR 构建 ────
└── unified(m, noders) // noder/noder.go:77
├── writePkgStub(m, noders) // noder/unified.go:195 → 318
│ └── checkFiles(m, noders) // noder/unified.go:319 → irgen.go:26
│ └── conf.Check(...) // irgen.go:95 types2 类型检查
├── readPackage(...) // noder/unified.go:200 !先写后读,使得本地/导入包共用一个读
├── r.pkgInit(...) // noder/unified.go:203
│ └── r.pkgDecls(target) // noder/reader.go:3326
│ └── target.Funcs = append(...) // noder/reader.go:3339 加入函数列表
└── readBodies(target, false) // noder/unified.go:205
└── pri.funcBody(fn) // noder/reader.go:1294
├── r.declareParams() // noder/reader.go:1309
└── r.stmts() // noder/reader.go:1654
└── typecheck.Stmt(n) // noder/reader.go:1667 IR 类型检查
──── 4. 中端优化 ────
interleaved.DevirtualizeAndInlinePackage(...) // gc/main.go:240 去虚化 + 内联(交替运行:去虚化后可内联,内联暴露更多去虚化机会)
noder.MakeWrappers(...) // gc/main.go:242
loopvar.ForCapture(fn) // gc/main.go:247 循环变量捕获修正
pkginit.MakeTask() // gc/main.go:252 生成包初始化任务
symABIs.GenABIWrappers() // gc/main.go:256 生成 ABI 包装函数 ABI0<->ABIInternal
deadlocals.Funcs(...) // gc/main.go:258 死局部变量消除
escape.Funcs(...) // gc/main.go:269 逃逸分析
──── 5-8. 后端(per function)────
for ... { // gc/main.go:290
enqueueFunc(fn) // gc/main.go:307
└── prepareFunc(fn) // gc/compile.go:90
└── walk.Walk(fn) // gc/compile.go:115 Walk
compileFunctions(profile) // gc/main.go:316
└── ssagen.Compile(fn, worker, profile) // ssagen/pgen.go:303
├── buildssa(fn, ...) // ssagen/pgen.go:304 IR → SSA
│ └── ssa.Compile(f) // SSA 优化
├── genssa(f, pp) // ssagen/pgen.go:314 SSA → 机器码
│ └── liveness.Compute(...) // ssagen/ssa.go:6341 GC stack map
└── pp.Flush() // ssagen/pgen.go:329 汇编、写目标文件
}
──── 9. 写目标文件 ────
dumpdata() // gc/main.go:352 收集元数据
dumpobj() // gc/main.go:354 写出 .o 文件
1.6 · 编译细节
1.6.1 · 1. 词法与语法分析#
- 包:
cmd/compile/internal/syntax
词法分析(Lexical Analysis):
- 工具:
syntax.Token和syntax.Scanner - 过程:源代码 → Token 流
- 位置信息:用于错误报告和调试
语法分析(Syntax Analysis):
-
工具:
syntax.Parser和syntax.File -
入口函数:
syntax.Parse() -
SourceFile = PackageClause ";" { ImportDecl ";" } { TopLevelDecl ";" } . TopLevelDecl = Declaration | FunctionDecl | MethodDecl . Declaration = ConstDecl | TypeDecl | VarDecl .
最终得到:
// compile/internal/syntax/nodes.go
//
// package PkgName; DeclList[0], DeclList[1], ...
type File struct {
Pragma Pragma // 存储文件开头的 //go: 指令信息
PkgName *Name // 包名,对应 `package xxx` 声
DeclList []Decl // 所有顶层声明的列表:导入、常量、变量、类型、函数(方法)
GoVersion string // 文件要求的最低 Go 版本
}
1.6.2 · 2. 类型检查(Semantic Analysis)#
入口:types2/check.go 中的 Checker.checkFiles() 方法,由 noder/irgen.go 中的 checkFiles() 函数调用
类型检查的三个核心任务:
-
标识符解析(Identifier Resolution):标识符 → Object
- 将 AST 中的标识符映射到声明的对象
-
类型推导(Type Deduction):表达式 → Type
- 确定每个表达式的类型
-
常量求值(Constant Evaluation):常量表达式 → Value
- 在编译期计算常量的值
这三个任务相互依赖:
- 常量值的确定可能依赖类型
- 表达式的类型可能依赖常量的值
- 标识符的解析可能依赖类型信息(如结构体字段)
关键数据结构:
Object(符号对象):types2/object.go
- 代表所有命名实体(const、var、type、func 等)
- 每个 AST 标识符映射到一个 Object
- 接口定义:
type Object interface { Name() string // 包级别名称 Exported() bool // 是否以大写字母开头 Type() Type // 对象类型 Pos() token.Pos // 声明中标识符的位置 Parent() *Scope // 声明该对象的作用域 Pkg() *Package // 所在包,nil 表示 Universe scope Id() string // 对象 ID(用于唯一性) }
Scope(作用域):types2/scope.go
- 静态作用域(词法作用域)
- 分层:全局 → 包 → 文件 → 代码块
- 存放 Object,支持名称查找
Type(类型):types2/type.go 及相关文件
- 基础类型、复合类型、结构体、接口、Named、Tuple 等
- 类型等价性:
Identical()函数(结构一致则等价) - 类型比较:可比较(等/不等)、可排序(大小等)
Checker(检查器):types2/check.go
- 核心方法:
checkFiles() - 维护类型检查的状态和信息
类型检查流程:
checkFiles()
├─ initFiles(验证同一包)
├─ collectObjects(AST → Object + declInfo)
├─ packageObjects(递归检查 check.objMap 中的对象)
│ └─ check.objDecl() 对每个对象进行检查
│ ├─ 类型表达式:Checker.typ()
│ ├─ 求值表达式:Checker.exprInternal()
│ └─ 类型兼容性:Checker.assignment()
├─ processDelayed(处理延迟队列)
│ └─ 函数体检查、循环依赖、接口整理
└─ initOrder(确定全局初始化顺序)
包加载与导出数据:
- 通过 Importer 接口的 Import 方法导入其他包
- 读取编译输出的对象文件(.a 或 .o)中的 export data
- 静态链接:链接器根据重定位信息规划内存
1.6.3 · 3. 中间代码生成(IR Tree)#
包:src/cmd/compile/internal/ir
作用:
- AST 是源代码的直接表示,信息不完整
- IR Tree 是信息更丰富的数据结构,包含编译信息
- 便于后续优化和代码生成
关键接口和结构:
-
Node 接口:实现 Node 的各种类型
- expr.go:表达式节点
- stmt.go:语句节点
- func.go:函数节点
- name.go:名称节点
-
Name 结构:变量/常量名称及其信息
-
Func 结构:函数定义及其信息
-
Op 类型:操作类型(如加法、调用等)
构建逻辑:由 irgen.generate() 驱动
1.6.4 · 中端:优化阶段
Go 编译器在多个阶段进行优化,包括:
1.6.4.1 · 前期优化(在 Unified IR 写入阶段)#
- 早期死代码消除:在 unified IR 写入过程中集成
1.6.4.2 · 中期优化(IR 级别,Walk 前)#
- 内联、逃逸分析、死代码消除
- 去虚拟化:用实际类型替换接口调用
1.6.5 · Walk 阶段#
包:cmd/compile/internal/walk
目的:
- 拆解复杂语句:将复杂语句分解为更简单的语句,引入临时变量
- 反糖(Desugaring):将高级构造转换为低级构造
switch→ 二分查找或跳转表for-range→ 简单 for 循环map/channel操作 → 运行时调用
执行时间:在 SSA 生成前,是 IR 上的最后一个遍历
1.6.6 · 包初始化
包:
src/cmd/compile/internal/pkginit:初始化逻辑src/cmd/compile/internal/staticinit:静态 vs 动态初始化判断
初始化顺序:
- 依赖包的初始化
- 全局变量初始化(编译时确定 vs 运行时)
- init 函数初始化
优化:尽可能在编译期完成变量初始化
1.6.7 · 优化阶段
1.6.7.1 · 1. Dead Code 消除#
包:src/cmd/compile/internal/deadcode
- 入口:
deadcode.Func() - 作用:
- 减小可执行文件大小
- 提升缓存命中率(增加局部性)
- 例:移除 if 分支中永不执行的代码、布尔运算的冗余分支
1.6.7.2 · 2. 函数内联(Inline)#
包:src/cmd/compile/internal/inline
原理:
- 函数调用有固定开销(上下文切换)
- 使用函数体替换函数调用以消除开销
优缺点:
- 优点:缩短调用链,提升性能
- 缺点:增大可执行文件体积,错误信息展示不友好
过程:
- 遍历函数调用链(
ir/scc.go的VisitFuncsBottomUp) - 计算函数复杂度(
hairyVisitor结构体) - 判断是否可以内联
- 进行内联(
InlineCalls函数)
优化策略:
- 一个函数被频繁调用但复杂度高,不会被内联
- 解决方案:抽出复杂代码为单独函数,降低原函数复杂度,提高内联机会
1.6.7.3 · 3. 逃逸分析(Escape Analysis)#
包:src/cmd/compile/internal/escape
目标:
- 确定变量应该分配在栈还是堆上
- 栈:快,生命周期与函数一致,自动释放
- 堆:慢,需要 GC 管理
分析规则:
- 指向栈上对象的指针不能在堆上
- 指向栈上对象的指针的生命周期不能大于其指向的对象
- 大对象分配到堆上
算法:基于 AST 的静态数据流分析
1.6.8 · 后端详解
1.6.8.1 · 1. SSA(Static Single Assignment)生成#
包:src/cmd/compile/internal/{ssagen,ssa}
入口:ssagen.Compile() 函数和 ssa.Compile() 函数
SSA 特点:
- 每个变量只赋值一次
- 低级中间表示,便于实现优化
- 便于进行数据流分析
过程:
- IR →
buildssa()函数(ssagen/ssa.go)将 IR 转换为 SSA - 通过 insertPhis 函数处理 phi 节点
- 调用
ssa.Compile()进行 SSA 优化 - 输出:SSA 值(Value)和块(Block)形成的 SSA 图
1.6.8.2 · 2. SSA 优化#
机器无关优化(Generic Passes):
- 死代码消除:移除未使用的计算
- 空指针检查移除:消除冗余的 nil 检查
- 未使用分支移除:删除未被取用的控制流分支
- 表达式优化:
- 常量折叠:
1 + 2→3 - 乘法优化:
x * 2→x << 1 - 浮点运算优化
- 常量折叠:
实现方式:
- 直接在 Go 代码中实现的 passes
- 通过 rewrite rules(
cmd/compile/internal/ssa/_gen/*.rules)代码生成的优化规则
关键机制:
- Lower Pass:将通用 SSA 转换为机器相关的变体(如 amd64 特定操作)
- Rewrite Rules:使用 S-expression 形式的 DSL 描述,自动生成优化代码
- 完整优化流程:从高级 SSA 开始,经过多个 passes,最终得到机器相关的 SSA
1.6.8.3 · 3. 机器码生成#
流程:
SSA → Lower Pass(SSA → 机器相关变体)
↓
最终优化(dead code、值移动、寄存器分配)
↓
栈帧布局(分配栈偏移给局部变量)
↓
obj.Prog 指令流
↓
汇编器(cmd/internal/obj)
↓
机器码 → 目标文件
最终文件内容:
- 机器码
- Reflect 数据(用于反射)
- Export 数据(用于导入)
- 调试信息(DWARF)
1.6.8.4 · 4. ABI(应用二进制接口)#
- 基于寄存器的 ABI(替代栈传参)
- 定义参数/返回值如何通过寄存器传递
- 优化函数调用性能
1.7 · 编译流程总结
1.7.1 · 完整编译链
go build main.go
↓
Go 命令行调度编译
↓
go tool compile main.go → main.o
├─ 词法分析:source → tokens
├─ 语法分析:tokens → AST (syntax.File)
├─ 类型检查:AST → Object + Type (types2)
├─ IR 生成(Noding):types2 → Unified IR → compiler IR
├─ 中端优化:内联、逃逸分析、死代码消除(IR 级)
├─ Walk:反糖、拆解、高级构造转换
├─ SSA 生成:IR → SSA
├─ SSA 优化:机器无关优化 (Generic Passes)
├─ SSA 降级:通用 SSA → 机器相关 SSA (Lower Pass)
├─ 最终优化:死代码消除、寄存器分配、指令调度
└─ 机器码:SSA → obj.Prog → 目标文件 main.o
↓
go tool link main.o → main
├─ 读取所有 .o 文件和 export data
├─ 符号解析和重定位
├─ 内存布局
└─ 生成可执行文件
↓
main(可执行文件)
1.7.2 · 优化发生的位置
- Unified IR 写入阶段:早期死代码消除
- IR 级(Walk 前):内联、逃逸分析、死代码消除、去虚拟化
- SSA 级(Generic Passes):机器无关优化(常量折叠、分支消除、nil 检查移除等)
- SSA 降级后:机器相关优化(如 amd64 特定的指令组合)
- 最终优化:寄存器分配、栈帧布局、指令调度、活跃指针分析