编译阶段
源代码------编译器前端------优化器------编译器后端------目标程序
编译器前端主要专注于理解源程序、扫描解析源程序并进行精准的语义表达。编译器的中间阶段(Intermediate Representation,IR)可能有多个,编译器会使用多个IR阶段、多种数据结构表示代码,并在中间阶段对代码进行多次优化。例如,识别冗余代码、识别内存逃逸等。编译器的中间阶段离不开编译器前端记录的细节。编译器后端专注于生成特定目标机器上的程序,这种程序可能是可执行文件,也可能是需要进一步处理的中间形态obj文件、汇编语言等
Go语言编译器的执行流程可细化为多个阶段,包括词法解析、语法解析、抽象语法树构建、类型检查、变量捕获、函数内联、逃逸分析、闭包重写、遍历函数、SSA生成、机器码生成
词法分析
词法分析是编译过程的第一步,主要任务是将Go源代码文件转换成一系列有意义的符号(tokens)。这些符号是编译器后续处理的基础。词法分析器会跳过源代码中的空白字符和注释,识别关键字、标识符、常量、运算符等,并将它们转换为相应的token。例如,func 关键字会被识别为一个表示函数定义的token。
语法分析
语法分析,又称为解析,是在词法分析之后进行的步骤。此阶段,编译器根据Go语言的语法规则,将token序列转换为抽象语法树(AST)。Go语言采用了一种称为自上而下的递归下降解析的方法,这种方法的特点是简单高效,能够避免解析过程中出现回溯现象。这意味着一旦选择了某个解析路径,就不会回头重新尝试其他可能的路径,从而提高了解析效率。
抽象语法树构建
抽象语法树(Abstract Syntax Tree, AST)是源代码的一种中间表示形式,它以树状结构展示了程序的逻辑结构。在Go语言中,每个import、type、const、func声明都会成为AST的一个根节点,而这些声明的具体内容则作为子节点存在。每个节点都有一个Op字段,这个字段定义在gc/syntax.go文件中,通常以字母O开头,用于标识节点的操作类型。Op字段不仅是一个整数,还包含了丰富的语义信息,帮助编译器理解每个节点的功能。
类型检查
类型检查是编译过程中的关键环节,旨在确保程序的类型正确性。编译器会遍历AST,检查每个节点的类型。这包括了显式声明的类型(例如 var a int)以及通过类型推断获得的类型(例如 a := 1)。对于后者,编译器会自动推断出变量a的类型为int。此外,编译器还会执行一些特殊的语法和语义检查,比如:
- 确保结构体字段的引用符合可见性规则,即只有大写的字段名才能被外部包访问。
- 检查数组字面量的访问是否超出了其边界。
- 验证数组索引是否为有效的正整数。
变量捕获
类型检查阶段完成后,Go语言编译器将对抽象语法树进行分析及重构,
变量捕获主要是针对闭包场景而言的,由于闭包函数中可能引用闭包外的变量,因此变量捕获需要明确在闭包中通过值引用或地址引用的方式来捕获变量。
函数内联
函数内联指将较小的函数直接组合进调用者的函数,可以减少函数调用带来的开销,函数调用的成本在于参数与返回值栈复制、较小的栈寄存器开销以及函数序言部分的检查栈扩容
go test 5_inline_test.go -bench=.
当函数内部有for、range、go、select等语句时,该函数不会被内联,当函数执行过于复杂(例如太多的语句或者函数为递归函数)时,也不会执行内联
函数内联是指将小型函数的实现直接嵌入到调用该函数的地方,从而减少函数调用所带来的额外开销。具体来说,这种做法可以消除参数传递、栈帧创建和返回值处理等操作所需的时间,进而提升程序的执行效率。内联通常适用于那些调用频繁且代码短小的函数,但如果函数内部包含复杂逻辑或循环等控制结构,则一般不会进行内联处理。
逃逸分析
用于标识变量内存应该被分配在栈区还是堆区,自动将该变量放置到堆区,并借助Go运行时的垃圾回收机制自动释放内存。编译器会尽可能地将变量放置到栈中,因为栈中的对象随着函数调用结束会被自动销毁,减轻运行时分配和垃圾回收的负担。
原则1:指向栈上对象的指针不能被存储到堆中
原则2:指向栈上对象的指针不能超过该栈对象的生命周期
- 抽象语法树(AST) :
- 编译器首先将源代码解析成抽象语法树(AST),这是一个表示程序结构的树形数据结构。
- AST 包含了程序的所有语法元素,如变量声明、函数调用、表达式等。
- 静态数据流分析 :
- 静态数据流分析是一种编译器技术,用于分析程序中数据的流动情况。
- 通过分析数据的流动,编译器可以确定哪些对象在函数调用结束后仍然被引用,哪些对象只在函数内部使用。
- 如果一个对象在函数调用结束后仍然被引用,那么这个对象需要在堆上分配。
- 如果一个对象只在函数内部使用,并且不会被外部引用,那么这个对象可以在栈上分配。
- 带权重的有向图 :
- 逃逸分析构建了一个带权重的有向图,其中节点表示对象,边表示对象之间的引用关系。
- 权重表示对象的引用强度或重要性。
- 权重较高的节点表示对象的引用强度较大,可能需要在堆上分配。
Go语言通过对抽象语法树的静态数据流分析(static data-flow analysis)来实现逃逸分析,这种方式构建了带权重的有向图。
可以通过在编译时加入-m=2标志打印出编译时的逃逸分析信息
当权重大于0时,代表存在*解引用操作。当权重为-1时,代表存在&引用操作。
节点代表变量,边代表变量之间的赋值,箭头代表赋值的方向,边上的数字代表当前赋值的引用或解引用的个数。节点的权重=前一个节点的权重+箭头上的数字
闭包重写
闭包重写分为闭包定义后被立即调用和闭包定义后不被立即调用两种情况。在闭包被立即调用的情况下,闭包只能被调用一次,这时可以将闭包转换为普通函数的调用形式。
如果闭包定义后不被立即调用,而是后续调用,那么同一个闭包可能被调用多次,这时需要创建闭包对象。
遍历函数
在该阶段会识别出声明但是并未被使用的变量,遍历函数中的声明和表达式,将某些代表操作的节点转换为运行时的具体函数执行
在执行walk函数遍历之前,编译器还需要对某些表达式和语句进行重新排序,
将x/=y替换为x=x/y。根据需要引入临时变量,以确保形式简单,例如x=m[k]或m[k]=x,而k可以寻址。
SSA生成
编译器会将抽象语法树转换为下一个重要的中间表示形态,称为SSA(Static Single Assignment,静态单赋值,在SSA生成阶段,每个变量在声明之前都需要被定义,并且,每个变量只会被赋值一次。
识别出无效的代码并将其清除。
汇编器
在SSA后,编译器将调用与特定指令集有关的汇编器(Assembler)生成obj文件,obj文件作为链接器(Linker)的输入,生成二进制可执行文件
链接
链接就是将编写的程序与外部程序组合在一起的过程。链接分为静态链接与动态链接,
静态链接的特点是链接器会将程序中使用的所有库程序复制到最后的可执行文件中,
而动态链接只会在最后的可执行文件中存储动态链接库的位置,并在运行时调用。因此静态链接更快,并且可移植,它不需要运行它的系统上存在该库,但是它会占用更多的磁盘和内存空间。静态链接发生在编译时的最后一步,动态链接发生在程序加载到内存时
Go语言在默认情况下是使用静态链接的
链接编译后
window是.exe
Linux是ELF(执行且可链接):机器码,调试信息、动态链接库信息、符号表信息