为了学好Golang底层知识装逼,折腾了一下编译器相关知识。下面的内容并不会提升你的生产技能点,但可以提高你的装逼指数。请按需阅读!
本文目录速览:
认识 go buildgo build
这个命令会编译go代码,今天就来一起看看go的编译过程吧!
首先先来认识以下go的代码源文件分类
_test.go
go build
接下来就用一个 hello world 程序来演示以下上面的命令选项。
go build -n
来分析下整个执行过程
compilebuildidlinka.out
mv
compilebuildid、link
编译器原理这是go编译器的源码路径
如上图所见,整个编译器可以分为:编译前端与编译后端;现在我们看看每个阶段编译器都做了些什么事情。先来从前端部分开始。
词法分析
Token
GolangToken
Token
首先先来给Go的token类型分个类:变量名、字面量、操作符、分隔符以及关键字。我们需要把一堆源代码按照规则进行拆分,其实就是分词,看着上面的例子代码我们可以大概制定一个规则如下:
()
TokenGolanglex
语法分析
TokenAST
TokenAST
- 自上而下
TokenSTRINGfunc
- 自下而上
这种是与上一种方式相反的,它先构造子树,然后再组装成一颗完整的树。
ASTToken
ASTToken
这颗树构造后,我们可以看到不同的类型是由对应的结构体来进行表示的。这里如果有语法、词法错误是不会被解析出来的。因为到目前为止说白了都是进行的字符串处理。
语义分析
编译器里边都把语法分析后的阶段叫做 语义分析,而go的这个阶段叫 类型检查;但是我看了以下go自己的文档,其实做的事情没有太大差别,我们还是按照主流规范来写这个过程。
那么语义分析(类型检查)究竟要做些什么呢?
AST
Golang
大意是:生成AST之后是类型检查(也就是我们这里说的语义分析),第一步是进行名称检查和类型推断,签定每个对象所属的标识符,以及每个表达式具有什么类型。类型检查也还有一些其它的检查要做,像“声明未使用”以及确定函数是否中止。
这一段是说:AST也会进行转换,有些节点根据类型信息进行精简,比如从算术加法节点类型中拆分出字符串加法。其它一些例子像dead code的消除,函数调用内联和逃逸分析。
上面两段文字来自 golang compile
这里多说一句,我们常常在debug代码的时候,需要禁止内联,其实就是操作的这个阶段。
# 编译的时候禁止内联
go build -gcflags '-N -l'
-N 禁止编译优化
-l 禁止内联,禁止内联也可以一定程度上减小可执行程序大小
经过语义分析之后,就可以说明我们的代码结构、语法都是没有问题的。所以编译器前端主要就是解析出编译器后端可以处理的正确的AST结构。
接下来我们看看编译器后端又有哪些事情要做。
机器只能够理解二进制并运行,所以编译器后端的任务简单来说就是怎么把AST翻译成机器码。
中间码生成
既然已经拿到AST,机器运行需要的又是二进制。为什么不直接翻译成二进制呢?其实到目前为止从技术上来说已经完全没有问题了。
makeslice64makeslice
中间码存在的另外一个价值是提升后端编译的重用,比如我们定义好了一套中间码应该是长什么样子,那么后端机器码生成就是相对固定的。每一种语言只需要完成自己的编译器前端工作即可。这也是大家可以看到现在开发一门新语言速度比较快的原因。编译是绝大部分都可以重复使用的。
而且为了接下来的优化工作,中间代码存在具有非凡的意义。因为有那么多的平台,如果有中间码我们可以把一些共性的优化都放到这里。
Golang
代码优化
在go的编译文档中,我并没找到独立的一步进行代码的优化。不过根据我们上面的分析,可以看到其实代码优化过程遍布编译器的每一个阶段。大家都会力所能及的做些事情。
通常我们除了用高效代码替换低效的之外,还有如下的一些处理:
- 并行性,充分利用现在多核计算机的特性
- 流水线,cpu有时候在处理a指令的时候,还能同时处理b指令
- 指令的选择,为了让cpu完成某些操作,需要使用指令,但是不同的指令效率有非常大的差别,这里会进行指令优化
- 利用寄存器与高速缓存,我们都知道cpu从寄存器取是最快的,从高速缓存取次之。这里会进行充分的利用
机器码生成
经过优化后的中间代码,首先会在这个阶段被转化为汇编代码(Plan9),而汇编语言仅仅是机器码的文本表示,机器还不能真的去执行它。所以这个阶段会调用汇编器,汇编器会根据我们在执行编译时设置的架构,调用对应代码来生成目标机器码。
GolangGOARCH=xxx
cmd/compile/main.go:main()
var archInits = map[string]func(*gc.Arch){
"386": x86.Init,
"amd64": amd64.Init,
"amd64p32": amd64.Init,
"arm": arm.Init,
"arm64": arm64.Init,
"mips": mips.Init,
"mipsle": mips.Init,
"mips64": mips64.Init,
"mips64le": mips64.Init,
"ppc64": ppc64.Init,
"ppc64le": ppc64.Init,
"s390x": s390x.Init,
"wasm": wasm.Init,
}
func main() {
// 从上面的map根据参数选择对应架构的处理
archInit, ok := archInits[objabi.GOARCH]
if !ok {
......
}
// 把对应cpu架构的对应传到内部去
gc.Main(archInit)
}
cmd/internal/obj/plist.go
func Flushplist(ctxt *Link, plist *Plist, newprog ProgAlloc, myimportpath string) {
... ...
for _, s := range text {
mkfwd(s)
linkpatch(ctxt, s, newprog)
// 对应架构的方法进行自己的机器码翻译
ctxt.Arch.Preprocess(ctxt, s, newprog)
ctxt.Arch.Assemble(ctxt, s, newprog)
linkpcln(ctxt, s)
ctxt.populateDWARF(plist.Curfn, s, myimportpath)
}
}
整个过程下来,可以看到编译器后端有很多工作需要做的,你需要对某一个指令集、cpu的架构了解,才能正确的进行翻译机器码。同时不能仅仅是正确,一个语言的效率是高还是低,也在很大程度上取决于编译器后端的优化。特别是即将进入AI时代,越来越多的芯片厂商诞生,我估计以后对这方面人才的需求会变得越来越旺盛。
总结总结一下学习编译器这部分古老知识带给我的几个收获:
Golang
本文的很多信息都来自下面的资料。