go的编译过程简单分析

734 阅读4分钟

理解go编译原理基础知识

  • 抽象语法树 AST
    • 例子 image_1.png
    • 抽象语法树:是很多语言对代码解析编译常用的数据结构,抽象语法会去掉源代码中不是特别重要的元素,如空格,分号,括号等等。
    • 编译器在执行完语法分析之后会输出一个抽象语法树,这个抽象语法树会辅助编译器进行语义分析,我们可以用它来确定语法正确的程序是否存在一些类型不匹配的问题
  • 静态单赋值SSA
    • 在编译器的设计中,静态单赋值形式(static single assignment form,通常简写为SSA form或是SSA)是中介码(IR,intermediate representation)的特性,每个变数仅被赋值一次 ;可以理解为是一种规范
    • SSA的理解
    1.如果每个变量在程序中有且只有一个赋值语句,那么该程序是SSA形式
    2.对于下面的代码,解释为什么需要SSA:
      x = 1
      y = x +1
      x = 2
      z = x + 1
      // 由于上面代码  y和z都等于x+1,那么编译器就会理解成 z = y,但是事实上 在z=x+1之前,对x重新赋值一次,所以z <> y的,所以就出现了一种形式(SSA),在编译器编译过程中,会将上面代码转换成下面的形式
      x1 = 1
      y1 = x1 + 1
      x2 = 2
      z1 = x2 + 1
      // 由于这样的转换,就不会造成让编译器理解成 z = y了,从而也会实现一些优化的作用,如下代码:
      x = 1
      x = 2
      y = x
      // SSA转换之后
      x1 = 1
      x2 = 2
      y1 = x2
      // 这样就会省去 对x=1的执行过程,因为x=1和下面没什么关系
      // 我们可以清晰地发现变量 y1 和 x1 是没有任何关系的,所以在机器码生成时就可以省去 x := 1 的赋值,通过减少需要执行的指令优化这段代码
    
    

理解go编译原理编译器过程

Go 语言编译器的源代码在 src/cmd/compile 目录中,目录下的文件共同组成了 Go语言的编译器

image_1.png

  • 大概过程:

    • 1、词法分析—-源文件通过词法分析将代码转成token的形式,执行词法分析的程序称为词法解析器
    package main
    import (
      "fmt"
    )
    func main() {
      fmt.Println("hello world")
    }
    // 转成
    _Package Ident
    _Import _Lparen
      quote Ident quote
    _Rparen
    _Func Ident _Lparen _Rparen
      _Lbrace
        Ident _Dot Ident _Lparen STRING
        _Rparen
      _Rbrace
     // 最后转成的 token内容:
     _Package,Ident,_Import,_Lparen...
    
    
    • 2、语法分析—-生成的token通过语法分析器顺序解析,该过程会讲词法分析生成的token根据编程语言定义好的语法(grammar)自下而上或者自上而下的规约解析,最终go的源文件最终会生成一个SourceFile结构,这个结构就是AST(抽象语法树)
    // 如下即为语法分析器Parser,根据词法分析器输出的Token序列,转化的抽象语法树
    “main.go”: SourceFile {
      PackageName: “main”,
      ImportDecl: []Import{
        “fmt”,
      },
      TopLevelDecl: ..
    }
    
    
    • 3、类型检查—-go在编译期会对抽象语法树中的内容,进行类型检查,而类型检查会按照下面的顺序分别验证和处理不同类型的节点
        1. 常量、类型和函数名及类型;
        2. 变量的赋值和初识化;
        3. 函数和闭包的主体;
        4. 哈希键值对的类型;
        5. 导入函数体;
        6. 外部的声明;
        7. 在这个阶段不止会对节点的类型进行验证,还会对一些内置的函数进行替换;如下就是make的替换å
    
    

    image_2.png

    • 4、中间代码生成— -在对整棵树的语法进行解析并进行类型检查之后,就可以确定代码是不存在语法错误或者类型错误的问题;在这个时候go就会把抽象树转化为中间代码;中间件代码则就使用了之前提到的;SSA特性
    // 编译器会通过cmd/compile/internal/gc.compileFunctions 编译整个GO语言中的全部函数,而这些函数都是会交给Goroutine进行处理;最终通过compileSSA转化为SSA
    func compileFunctions() {
      if len(compilequeue) != 0 {
      // ..
        var wg sync.WaitGroup
        Ctxt.InParallel = true
        c := make(chan *Node, nBackendWorkers)
        for i := 0; i < nBackendWorkers; i++ {
          wg.Add(1)
          go func(worker int) {
            for fn := range c {
              compileSSA(fn, worker)
            }
            wg.Done()
          }(i)
        }
        // ..
        close(c)
        compilequeue = nil
        wg.Wait()
      }
    }
    
    
    • 5、机器码生成—中间代码;先转化为汇编代码,然后调用汇编器,汇编器会根据我们在执行编译时设置的架构,调用对应的代码来生成目标机器码

    • 注意:在生成中间代码之后,不立即转化为最终执行代码是因为不确定当前的操作系统,不同的操作系统会效果不一样

    • 6、最终执行代码

  • 最后总结 image_3.png