查看 Go 的代码优化过程
之前有人在某群里询问 Go 的编译器是怎么识别下面的代码始终为 false,并进行优化的:
package main func 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 main import ( "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:1 12 . . Params: *ast.FieldList { 13 . . . Opening: ./com.go:3:10 14 . . . Closing: ./com.go:3:11 15 . . } 16 . } 17 . Body: *ast.BlockStmt { 18 . . Lbrace: ./com.go:3:13 19 . . List: []ast.Stmt (len = 2) { 20 . . . 0: *ast.DeclStmt { 21 . . . . Decl: *ast.GenDecl { 22 . . . . . TokPos: ./com.go:4:2 23 . . . . . Tok: var 24 . . . . . 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:6 30 . . . . . . . . . Name: "a" 31 . . . . . . . . . Obj: *ast.Object { 32 . . . . . . . . . . Kind: var 33 . . . . . . . . . . Name: "a" 34 . . . . . . . . . . Decl: *(obj @ 26) 35 . . . . . . . . . . Data: 0 36 . . . . . . . . . } 37 . . . . . . . . } 38 . . . . . . . } 39 . . . . . . . Values: []ast.Expr (len = 1) { 40 . . . . . . . . 0: *ast.BasicLit { 41 . . . . . . . . . ValuePos: ./com.go:4:10 42 . . . . . . . . . Kind: INT 43 . . . . . . . . . Value: "1" 44 . . . . . . . . } 45 . . . . . . . } 46 . . . . . . } 47 . . . . . } 48 . . . . . Rparen: - 49 . . . . } 50 . . . } 51 . . . 1: *ast.IfStmt { 52 . . . . If: ./com.go:5:2 53 . . . . Cond: *ast.BinaryExpr { 54 . . . . . X: *ast.Ident { 55 . . . . . . NamePos: ./com.go:5:5 56 . . . . . . Name: "a" 57 . . . . . . Obj: *(obj @ 31) 58 . . . . . } 59 . . . . . OpPos: ./com.go:5:7 60 . . . . . Op: != 61 . . . . . Y: *ast.BasicLit { 62 . . . . . . ValuePos: ./com.go:5:10 63 . . . . . . Kind: INT 64 . . . . . . Value: "1" 65 . . . . . } 66 . . . . } 67 . . . . Body: *ast.BlockStmt { 68 . . . . . Lbrace: ./com.go:5:12 69 . . . . . List: []ast.Stmt (len = 1) { 70 . . . . . . 0: *ast.ExprStmt { 71 . . . . . . . X: *ast.CallExpr { 72 . . . . . . . . Fun: *ast.Ident { 73 . . . . . . . . . NamePos: ./com.go:6:3 74 . . . . . . . . . Name: "println" 75 . . . . . . . . } 76 . . . . . . . . Lparen: ./com.go:6:10 77 . . . . . . . . Args: []ast.Expr (len = 1) { 78 . . . . . . . . . 0: *ast.BasicLit { 79 . . . . . . . . . . ValuePos: ./com.go:6:11 80 . . . . . . . . . . Kind: STRING 81 . . . . . . . . . . Value: "\"oh no\"" 82 . . . . . . . . . } 83 . . . . . . . . } 84 . . . . . . . . Ellipsis: - 85 . . . . . . . . Rparen: ./com.go:6:18 86 . . . . . . . } 87 . . . . . . } 88 . . . . . } 89 . . . . . Rbrace: ./com.go:7:2 90 . . . . } 91 . . . } 92 . . } 93 . . Rbrace: ./com.go:8:1 94 . } 95 }
显然,到语法分析完毕之后,ast 中的 if 节点还活得好好的。只能看看后端部分了:
GOSSAFUNC=main go build com.go
SSA 的多轮优化就是编译原理里常说的后端优化,这一步是 deadcode opt,顾名思义。
dump 过程中可能会有权限问题:
# runtime : internal compiler error: 'main': open ssa.html: permission denied Please file a bug report including a short program that triggers the error. https://golang.org/issue/new
加个 sudo 就好。
既然 Go 是在编译后端进行的死代码消除,那么对于我们来说,想要复用编译器代码,并提前提示就不太方便了。从原理上来讲,我们仍然可以在遍历 ast 的时候存储一些常量、变量的值来进行前文中提出的需求。这就看你有没有兴趣去实现了。