前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >赠书福利 | 为什么要了解 Go 语言编译器?

赠书福利 | 为什么要了解 Go 语言编译器?

作者头像
米开朗基杨
发布2021-10-27 15:04:14
4890
发布2021-10-27 15:04:14
举报
文章被收录于专栏:云原生实验室云原生实验室

大家好,我是米开朗基杨。

编译器是一个大型且复杂的系统,一个好的编译器会很好地结合形式语言理论、算法、人工智能、系统设计、计算机体系结构及编程语言理论。Go语言的编译器遵循了主流编译器采用的经典策略及相似的处理流程和优化规则(例如经典的递归下降的语法解析、抽象语法树的构建)。另外,Go语言编译器有一些特殊的设计,例如内存的逃逸等。

编译原理值得用一本书的笔墨去讲解,通过了解Go语言编辑器,不仅可以了解大部分高级语言编译器的一般性流程与规则,也能指导我们写出更加优秀的程序。很多Go语言的语法特性都离不开编译时与运行时的共同作用。另外,如果读者希望开发go import、go fmt、go lint等扫描源代码的工具,那么同样离不开编译器的知识和Go语言提供的API。

Go语言编译器的阶段

如图1-1所示,在经典的编译原理中,一般将编译器分为编译器前端、优化器和编译器后端。这种编译器被称为三阶段编译器(three-phase compiler)。其中,编译器前端主要专注于理解源程序、扫描解析源程序并进行精准的语义表达。编译器的中间阶段(Intermediate Representation,IR)可能有多个,编译器会使用多个IR阶段、多种数据结构表示代码,并在中间阶段对代码进行多次优化。例如,识别冗余代码、识别内存逃逸等。编译器的中间阶段离不开编译器前端记录的细节。编译器后端专注于生成特定目标机器上的程序,这种程序可能是可执行文件,也可能是需要进一步处理的中间形态obj文件、汇编语言等。

图1-1 三阶段编译器

需要注意的是,编译器优化并不是一个非常明确的概念。优化的主要目的一般是降低程序资源的消耗,比较常见的是降低内存与CPU的使用率。但在很多时候,这些目标可能是相互冲突的,对一个目标的优化可能降低另一个目标的效率。同时,理论已经表明有一些代码优化存在着NP难题,这意味着随着代码的增加,优化的难度将越来越大,需要花费的时间呈指数增长。因为这些原因,编译器无法进行最佳的优化,所以通常采用一种折中的方案。

Go语言编译器一般缩写为小写的gc(go compiler),需要和大写的GC(垃圾回收)进行区分。Go语言编译器的执行流程可细化为多个阶段,包括词法解析、语法解析、抽象语法树构建、类型检查、变量捕获、函数内联、逃逸分析、闭包重写、遍历函数、SSA生成、机器码生成,如图1-2所示。

图1-2 Go语言编译器执行流程

词法解析

和Go语言编译器有关的代码主要位于src/cmd/compile/internal目录下,在后面分析中给出的文件路径均默认位于该目录中。在词法解析阶段,Go语言编译器会扫描输入的Go源文件,并将其符号(token)化。例如“+”和“-”操作符会被转换为_IncOp,赋值符号“:= ”会被转换为_Define。这些token实质上是用iota声明的整数,定义在syntax/tokens.go中。符号化保留了Go语言中定义的符号,可以识别出错误的拼写。同时,字符串被转换为整数后,在后续的阶段中能够被更加高效地处理。图1-3为一个示例,展现了将表达式a:=b + c(12)符号化之后的情形。代码中声明的标识符、关键字、运算符和分隔符等字符串都可以转化为对应的符号。

图1-3 Go语言编译器词法解析示例

Go语言标准库go/scanner、go/token也提供了许多接口用于扫描源代码。在下例中,我们将使用这些接口模拟对Go文本文件的扫描。

代码语言:javascript
复制
package main
import (
    "fmt"
    "go/scanner"
    "go/token"
)
func main() {
    src := []byte("cos(x) + 2i*sin(x) // Euler")

    // 初始化 scanner
    var s scanner.Scanner
    fset := token.NewFileSet()                      
    file := fset.AddFile("", fset.Base(), len(src)) 
    s.Init(file, src, nil , scanner.ScanComments) 
  // 扫描
    for {
        pos, tok, lit := s.Scan()
        if tok == token.EOF {
            break
        }
        fmt.Printf("%s\t%s\t%q\n", fset.Position(pos), tok, lit)
    }
}

在上例中,src为进行词法扫描的表达式,可以将其模拟为一个文件并调用scanner.Scanner词法,扫描后分别打印出token的位置、符号及其字符串字面量。每个标识符与运算符都被特定的token代替,例如2i被识别为复数IMAG,注释被识别为COMMENT。

代码语言:javascript
复制
1:1     IDENT   "cos"
1:4     (       ""
1:5     IDENT   "x"
1:6     )       ""
1:8     +       ""
1:10    IMAG    "2i"
1:12    *       ""
1:13    IDENT   "sin"
1:16    (       ""
1:17    IDENT   "x"
1:18    )       ""
1:20    ;       "\n"
1:20    COMMENT "// Euler"

语法解析

词法解析阶段结束后,需要根据Go语言中指定的语法对符号化后的Go文件进行解析。Go语言采用了标准的自上而下的递归下降(Top-Down Recursive-Descent)算法,以简单高效的方式完成无须回溯的语法扫描,核心算法位于syntax/nodes.go及syntax/parser.go中。图1-4为Go语言编译器对文件进行语法解析的示意图。在一个Go源文件中主要有包导入声明(import)、静态常量(const)、类型声明(type)、变量声明(var)及函数声明。

图1-4 Go语言编译器对文件进行语法解析的示意图

源文件中的每一种声明都有对应的语法,递归下降通过识别初始的标识符,例如_const,采用对应的语法进行解析。这种方式能够较快地解析并识别可能出现的语法错误。每一种声明语法在Go语言规范中都有定义

代码语言:javascript
复制
//包导入声明
ImportSpec = [ "." | PackageName ] ImportPath .
ImportPath = string_lit .
// 静态常量
ConstSpec = IdentifierList [ [ Type ] "=" ExpressionList ] .
//类型声明
TypeSpec = identifier [ "=" ] Type .
//变量声明
VarSpec = IdentifierList ( Type [ "=" ExpressionList ] | "=" ExpressionList ) .

函数声明是文件中最复杂的一类语法,因为在函数体的内部可能有多种声明、赋值(例如:= )、表达式及函数调用等。例如defer语法为defer Expression ,其后必须跟一个函数或方法。每一种声明语法或者表达式都有对应的结构体,例如a := b + f(89) 对应的结构体为赋值声明AssignStmt。Op代表当前的操作符,即“ :=”, Lhs与Rhs分别代表左右两个表达式。

代码语言:javascript
复制
AssignStmt struct {
        Op       Operator 
        Lhs, Rhs Expr     
        simpleStmt
    }

语法解析丢弃了一些不重要的标识符,例如括号“ (”,并将语义存储到了对应的结构体中。语法声明的结构体拥有对应的层次结构,这是构建抽象语法树的基础。图1-5为a := b + c(12)语句被语法解析后转换为对应的syntax.AssignStmt结构体之后的情形。最顶层的Op操作符为token.Def(:= )。Lhs表达式类型为标识符syntax.Name,值为标识符“a”。Rhs表达式为syntax.Operator加法运算。加法运算左边为标识符“b”,右边为函数调用表达式,类型为CallExpr。其中,函数名c的类型为syntax.Name,参数为常量类型syntax.BasicLit,代表数字12。

图1-5 特定表达式的语法解析示例

Go语言可执行文件的生成离不开编译器所做的复杂工作,如果不理解Go语言的编译器,那么对Go语言的理解是不完整的。Go语言的编译器、运行时,本身就是用Go语言写出的既复杂又精巧的程序;探究语言设计、语法特性,本身就是学习程序设计与架构、数据结构与算法等知识的绝佳途径。学习底层原理能够帮助我们更好地了解Go语言的语法,做出合理的性能优化,设计科学的程序架构,监控程序的运行状态,排查复杂的程序异常问题,开发出检查协程泄露、语法等问题的高级工具,理解Go语言的局限性,从而在不同场景下做出合理抉择。

编译阶段包括词法解析、语法解析、抽象语法树构建、类型检查、变量捕获、函数内联、逃逸分析、闭包重写、遍历并编译函数、SSA生成、机器码生成。编译器不仅能准确地表达语义,还能对代码进行一定程度的优化。可以看到,Go语言的很多语法检查、语法特性都依赖编译时。理解编译时的基本流程、优化方案及一些调试技巧有助于写出更好的程序。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-10-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 云原生实验室 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档