滴滴曹大:查看 Go 的代码优化过程

720 阅读4分钟
原文链接: mp.weixin.qq.com

点击上方蓝色“ Go语言中文网”关注我们,设个 星标,每天学习Go语言

之前有人在某群里询问 Go 的编译器是怎么识别下面的代码始终为 false,并进行优化的:

package mainfunc main() {    var a = 1    if a != 1 {        println("oh no")    }}

先说是不是,再说为什么。先看看他的结论对不对:

TEXT main.main(SB) /Users/xargin/test/com.go  com.go:3              0x104ea70               c3                      RET  .... 后面都是填充物

整个 main 函数的逻辑都被优化掉了,二进制文件中 main 函数什么都没干就直接 RET 了。说明在编译过程中,Go 的编译器确实会对这段无效代码进行优化。

之前有接触过 Go 的静态扫描工具的同学就问了,Go 编译器的这种优化我们能不能进行复用呢。把逻辑从编译器中抽出来,直接做个静态扫描工具来告诉你又写出了垃圾代码。

嗯,我们来看看到底行不行,首先需要简单理解 Go 的编译过程。

Go 从代码文本到可执行执行文件的编译过程大致为:

词法分析 ------------> 语法分析 ----------> 中间代码生成 ----------> 目标代码生成        token stream            ast                    SSA           asm

当前开源社区的静态扫描工具,分析的对象都是 ast,因为 Go 的 compiler 接口是开放的,所以我们可以直接用 go/parser 、 go/ast 库来生成这个 ast。之后再调用 Walk 来遍历语法树,或者我们自己写一个遍历 ast 的流程也不麻烦。在遍历过程中,可以根据单句代码(比如有个东西叫 ineff assign),或者根据代码的上下文来给出一些建议和警示(比如一些什么 go vet、gosimple 啊之类的东西)。

从词法分析到语法分析一般被称为编译器的前端(frontend),而中间代码生成和目标代码生成则是编译器后端(backend)。

所以不管怎么说,想做静态扫描,就是在和 ast 打交道,即在编译器前端折腾。这里的问题是,Go 的编译器对前述代码的优化究竟是在编译过程的哪一步进行的呢?

获得代码的 ast 很简单:

package mainimport (  "go/ast"  "go/parser"  "go/token")func main() {    fset := token.NewFileSet()    f, _ := parser.ParseFile(fset, "./demo.go", nil, parser.Mode(0))    for _, d := range f.Decls {        ast.Print(fset, d)    }}

输出 ast:

 0  *ast.FuncDecl { 1  .  Name: *ast.Ident { 2  .  .  NamePos: ./com.go:3:6 3  .  .  Name: "main" 4  .  .  Obj: *ast.Object { 5  .  .  .  Kind: func 6  .  .  .  Name: "main" 7  .  .  .  Decl: *(obj @ 0) 8  .  .  } 9  .  }10  .  Type: *ast.FuncType {11  .  .  Func: ./com.go:3:112  .  .  Params: *ast.FieldList {13  .  .  .  Opening: ./com.go:3:1014  .  .  .  Closing: ./com.go:3:1115  .  .  }16  .  }17  .  Body: *ast.BlockStmt {18  .  .  Lbrace: ./com.go:3:1319  .  .  List: []ast.Stmt (len = 2) {20  .  .  .  0: *ast.DeclStmt {21  .  .  .  .  Decl: *ast.GenDecl {22  .  .  .  .  .  TokPos: ./com.go:4:223  .  .  .  .  .  Tok: var24  .  .  .  .  .  Lparen: -25  .  .  .  .  .  Specs: []ast.Spec (len = 1) {26  .  .  .  .  .  .  0: *ast.ValueSpec {27  .  .  .  .  .  .  .  Names: []*ast.Ident (len = 1) {28  .  .  .  .  .  .  .  .  0: *ast.Ident {29  .  .  .  .  .  .  .  .  .  NamePos: ./com.go:4:630  .  .  .  .  .  .  .  .  .  Name: "a"31  .  .  .  .  .  .  .  .  .  Obj: *ast.Object {32  .  .  .  .  .  .  .  .  .  .  Kind: var33  .  .  .  .  .  .  .  .  .  .  Name: "a"34  .  .  .  .  .  .  .  .  .  .  Decl: *(obj @ 26)35  .  .  .  .  .  .  .  .  .  .  Data: 036  .  .  .  .  .  .  .  .  .  }37  .  .  .  .  .  .  .  .  }38  .  .  .  .  .  .  .  }39  .  .  .  .  .  .  .  Values: []ast.Expr (len = 1) {40  .  .  .  .  .  .  .  .  0: *ast.BasicLit {41  .  .  .  .  .  .  .  .  .  ValuePos: ./com.go:4:1042  .  .  .  .  .  .  .  .  .  Kind: INT43  .  .  .  .  .  .  .  .  .  Value: "1"44  .  .  .  .  .  .  .  .  }45  .  .  .  .  .  .  .  }46  .  .  .  .  .  .  }47  .  .  .  .  .  }48  .  .  .  .  .  Rparen: -49  .  .  .  .  }50  .  .  .  }51  .  .  .  1: *ast.IfStmt {52  .  .  .  .  If: ./com.go:5:253  .  .  .  .  Cond: *ast.BinaryExpr {54  .  .  .  .  .  X: *ast.Ident {55  .  .  .  .  .  .  NamePos: ./com.go:5:556  .  .  .  .  .  .  Name: "a"57  .  .  .  .  .  .  Obj: *(obj @ 31)58  .  .  .  .  .  }59  .  .  .  .  .  OpPos: ./com.go:5:760  .  .  .  .  .  Op: !=61  .  .  .  .  .  Y: *ast.BasicLit {62  .  .  .  .  .  .  ValuePos: ./com.go:5:1063  .  .  .  .  .  .  Kind: INT64  .  .  .  .  .  .  Value: "1"65  .  .  .  .  .  }66  .  .  .  .  }67  .  .  .  .  Body: *ast.BlockStmt {68  .  .  .  .  .  Lbrace: ./com.go:5:1269  .  .  .  .  .  List: []ast.Stmt (len = 1) {70  .  .  .  .  .  .  0: *ast.ExprStmt {71  .  .  .  .  .  .  .  X: *ast.CallExpr {72  .  .  .  .  .  .  .  .  Fun: *ast.Ident {73  .  .  .  .  .  .  .  .  .  NamePos: ./com.go:6:374  .  .  .  .  .  .  .  .  .  Name: "println"75  .  .  .  .  .  .  .  .  }76  .  .  .  .  .  .  .  .  Lparen: ./com.go:6:1077  .  .  .  .  .  .  .  .  Args: []ast.Expr (len = 1) {78  .  .  .  .  .  .  .  .  .  0: *ast.BasicLit {79  .  .  .  .  .  .  .  .  .  .  ValuePos: ./com.go:6:1180  .  .  .  .  .  .  .  .  .  .  Kind: STRING81  .  .  .  .  .  .  .  .  .  .  Value: "\"oh no\""82  .  .  .  .  .  .  .  .  .  }83  .  .  .  .  .  .  .  .  }84  .  .  .  .  .  .  .  .  Ellipsis: -85  .  .  .  .  .  .  .  .  Rparen: ./com.go:6:1886  .  .  .  .  .  .  .  }87  .  .  .  .  .  .  }88  .  .  .  .  .  }89  .  .  .  .  .  Rbrace: ./com.go:7:290  .  .  .  .  }91  .  .  .  }92  .  .  }93  .  .  Rbrace: ./com.go:8:194  .  }95  }

显然,到语法分析完毕之后,ast 中的 if 节点还活得好好的。只能看看后端部分了:

GOSSAFUNC=main go build com.go
deadcode

SSA 的多轮优化就是编译原理里常说的后端优化,这一步是 deadcode opt,顾名思义。

dump 过程中可能会有权限问题:

# runtime<unknown line number>: internal compiler error: 'main': open ssa.html: permission deniedPlease file a bug report including a short program that triggers the error.https://golang.org/issue/new

加个 sudo 就好。

既然 Go 是在编译后端进行的死代码消除,那么对于我们来说,想要复用编译器代码,并提前提示就不太方便了。从原理上来讲,我们仍然可以在遍历 ast 的时候存储一些常量、变量的值来完成前文中提出的需求。这就看你有没有兴趣去实现了。

推荐阅读

『GCTT 出品』剖析与优化 Go 的 web 应用

『GCTT 出品』在 Golang 中针对 int64 类型优化 abs()


如果觉得本文不错,欢迎关注我们