Go 编译器和静态分析 | 青训营笔记

15 阅读3分钟

编译器和静态分析

这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天。今天讲编译器的优化。

编译器是非常重要的系统软件,用来识别符合语法和非法的程序,生成正确且高效的代码、

编译器的结构

编译器的结构如下,分为前端和后端。

前端也叫分析部分,源代码输入语法分析,得到词素;语法分析器利用词素构建抽象语法树;语义分析收集类型信息,进行语义检查,得到了一棵装饰过的语法分析树。再做中间代码生成,得到中间表示intermediate representation(IR)。生成的中间表示是和机器无关的。

那么接下来就去做与机器无关的代码优化,生成优化以后的IR,最后就可以生成代码,可见后端是优化的重点。

静态分析

静态分析是编译器优化常用到的手段,它旨在不执行程序代码,推导程序行为,分析程序性质。它可以分为以下两个部分:

控制流:程序执行的流程。分析程序的执行流程,画出程序的控制流程图

数据流:数据在控制流上的传递。沿着控制流传播数据,我们就可以得到更多的关于程序的性质。 根据上面的性质,我们可以优化代码。

过程内和过程间的分析

过程内:仅在函数内部进行分析

过程间:要考虑谁调用谁? 通常要同时分析控制流和数据流。

在编译优化的时候常常采用函数内联的方式减少过程间的分析。下面我们会讲到。

Go 编译器优化

为什么要做编译器优化?

  • 用户无感知,重新编译就可以获得性能收益
  • 编译器优化是一种通用性优化

有时候编译优化常常会导致编译的时间更长,所以在编译时间和性能方面常常需要做tradeoff.常见的优化方法:

函数内联

将被调用的函数的函数体的副本换到调用的位置上,同时重写代码以反映参数的绑定。

优点:消除函数调用的开销,比如传递参数,保存寄存器等;另一方面,将过程间的分析转化为过程内的分析。

缺点: 函数体变大了,对instruction cache不友好; 同时Go编译生成的镜像也变大了

一般来说,内联的都是正向优化的,我们可以采取一些内联策略,比如根据调用者和被调函数的规模来确定是否内联。我们可以尝试micro-benchmark快速验证和对比性能优化结果。

逃逸分析

逃逸分析是指分析代码中指针得动态作用域,指针在何处可以被访问。 逃逸分析思路: 指针都是指向对象的,所以我们从对象分配处出发,沿着控制流,观察对象的数据流。 如果发现指针p在当前作用域s中:

  • 作为参数传递给其他函数
  • 传递给全局变量
  • 传递给其他的goroutine
  • 传递给已经逃逸的指针指向的对象

那么,该指针指向的对象逃逸出了s。 函数内联可以拓展函数边界,让更多的对象不逃逸。不逃逸的对象可以在栈上分配。而在栈上分配和回收对象都很快。另一方面,减少在heap上的分配可以降低GC(垃圾回收)的负担。

又是很有收获的一天,更多内容有待后续补充。