Go编译器简介【译】
趁着元旦休假+春节,尝试把2018年期间让我受益的一些文章、问答,翻译一下。
欢迎指正、讨论,希望对你也有所帮助。
原文链接:https://github.com/golang/go/...
构成Go编译器的关键package都包含在cmd/compile目录。我们从逻辑上把编译器编译过程分成四个阶段,下文将简要介绍这四个阶段的package列表。
谈到编译器,你可能听说过类似“前端”、“后端”这样的字眼。粗略地讲,我们也将编译器的四个阶段工作划分成了前两个阶段和后两个阶段,也就是前端和后端。还有一个词——”中端“,通常包含在第二个阶段中(译者注:有的编译器在前端和后端之间引入了一个代码优化阶段,称为middle-end,中端。感兴趣的读者可以提前深入了解下一般编译器架构)。
需要注意的是,go/目录的package和编译器无关,比如go/types等。由于最初的编译器是用C语言编写而成,go/中包含了一些用Go代码编写的工具,如gofmt和vet。
还有一点需要澄清,”gc“代表”Go compiler“,和我们常用来表示垃圾回收的GC没啥关系。
1.词法分析和语法分析
cmd/compile/internal/syntax目录包含了词法分析、语法分析和语法树构造部分。编译器第一阶段工作中,对每个源文件进行词法、语法分析,并生成AST(抽象语法树)。
每个语法树都精确表达了相应的源代码文件。树的节点对应着各个源文件的元素,例如表达式、声明语句。语法树还包含了代码位置信息,为错误报告和调试信息提供支持。
2.类型检查和AST转换
cmd/compile/internal/gc目录包含了编译器AST创建、类型检查、AST转换部分。这个package中包含了一个从C继承的AST定义。这个package的第一要事就是把syntax包的语法树转换成编译器支持的AST来表示。这个步骤看起来略显多余,在将来的版本中可能会被重构。
接下来要做的是类型检查。第一步做名字解析和类型推断,确定对象属于哪个标识符以及表达式的类型。类型检查还包括一些额外操作,例如判断”声明但未使用“的变量、确定函数是否会终止。
某些节点类型的细化也会在这部分完成。例如从算术加节点中拆分出字符串加、无用代码消除、函数调用内联化和逃逸分析。
3.通用SSA
这里有官方给出的SSA背景介绍。
cmd/compile/internal/gc(SSA转换)
cmd/compile/internal/ssa(SSA pass和规则)
这个阶段,AST将转换为静态单赋值(SSA)形式。这是一种具有特定属性的低级中间表示法,更容易优化和生成机器码。
转换期间还要处理内置函数。现代编译器经过多代进化已经很智能,会用大量优化代码替代内置函数,获得更高性能。
在AST到SSA转换期间,为了方便复用,一些节点也降级为更简单的组件。例如,内置拷贝(译者猜:copy?)替换内存移动(译者猜:memmove?)并将range重写为for。由于历史原因,其中一些在转换为SSA之前完成,未来计划全部移至这里。
然后,一系列与机器无关的pass和规则被应用。这些不涉及任何单一计算机体系结构。
这些环节包括消除无用代码、删除非必需的空值检查以及删除未使用的分支。通用重写规则主要涉及表达式,例如用常量替换、乘法和浮点运算的优化。
4.生成机器代码
cmd/compile/internal/ssa(SSA低级化和特定架构处理)
cmd/internal/obj(机器码生成)
机器依赖相关的阶段以比较“较低”的操作开始,这些操作将通用变量重写成其特定于机器的变体。例如,amd64架构中,可以合并许多加载存储操作。
要注意”低级“的操作运行所有机器特定的重写规则,它也应用了大量优化。
一旦SSA被“低级”处理并且更具体地针对目标体系架构,就要运行最终代码优化的处理步骤了。其中包含了另一个无用代码消除的步骤。该步骤会将变量移动到更靠近它们被使用的位置、删除从未被读取的局部变量以及寄存器分配。
此步骤完成的其他重要工作包括堆栈布局和指针分析。堆栈布局将堆栈偏移分配给局部变量,指针分析计算每个GC安全点上的堆栈指针是否仍然是活动的。
在SSA生成阶段结束时,Go函数已转换为sequence。这些sequence最终被转换成机器码。