go mode 查看依赖树_滴滴曹大:查看 Go 的代码优化过程

点击上方蓝色“Go语言中文网”关注我们,设个星标,每天学习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
099d49fed13503c18414131b12d1e6ee.png
deadcode

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 的时候存储一些常量、变量的值来完成前文中提出的需求。这就看你有没有兴趣去实现了。

推荐阅读

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

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


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

3db08e065ce3d5c6b43d142566579695.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值