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

103 阅读4分钟

这是我参与「第五届青训营 」笔记创作活动的第九天

一、本堂课重点内容:

  • 编译器和静态分析
  • Go编译器优化

二、详细知识点介绍:

编译器的结构

编译器是一个很重要的系统软件,它能够识别到不符合要求的代码拼写,而且可以帮助生成正确的代码

它主要有两个部分组成:

首先,是分析部分(前端 front end):

  • 词法分析,生成语速(lexeme)
  • 语法分析,生成语法树
  • 语义分析,收集类型信息,进行语义检查
  • 中间代码生成,生成intermediate representation(IR)

然后是综合部分(后端 back end)

  • 代码优化,机器无关优化,生成优化后的IR
  • 代码生成,生成目标代码 image.png 我们主要学习编译器后端优化

静态分析

接下来我们来了解有关静态分析的概念

静态分析指的是:不执行程序代码,推导程序的行为,分析程序的性质

然后我们可以通过分析控制流(程序执行的流程Control flow

数据流(数据在控制流上的传递Data flow

从而得到更多关于程序的性质

我们就可以通过这些性质来优化代码

过程内和过程间分析

接下来我们来了解一下有关过程内和过程间分析的概念:

  • 过程内分析(Intra-procedural analysis):就是仅在函数内部进行分析
  • 过程间分析(Inter-procedural analysis):考虑过程调用时参数传递和返回值的数据流和控制流

那为啥过程间分析是一个问题呢?我们以下面这个例子作解释:

image.png 我们如果想要分析这个i.foo()的话

我们首先得需要通过数据流分析得知i得具体类型,然后才能知道i.foo()调用的是哪个foo()

之后我们根据i的具体类型,产生了新的控制流A.foo(),分析继续······

由上面的例子不难看出——过程间分析需要同时分析控制流和数据流,所以就会联合求解,比较复杂

Go编译器优化

首先我们得了解为什么我们要做编译器优化,好处在哪:

  1. 用户无感知,重新编译即可获得性能收益
  2. 通用性优化

目前对于编译器优化的现状:

  • 采用的优化少
  • 编译时间较短,没有进行较复杂的代码分析和优化

对于编译器优化面对的场景:面对后端长期执行任务

优化思路:用编译时间换取更高效的机器码

比较好的优化模式:

  • 函数内联
  • 逃逸分析
  • 默认栈大小调整
  • 边界检查消除
  • 循环展开
  • ······

接下来,我们来了解一下函数内联逃逸分析

函数内联(InIning):

函数内联就是将被调用函数的函数体(callee)的副本替换到调用的位置(caller)上,同时重写代码以反映参数的绑定。

优点:

  • 消除函数调用的开销,例如传递参数、保存寄存器等
  • 将过程间分析转化为过程内分析,帮助其他优化,例如逃逸分析

缺点:

  • 函数体变大,instruction cache(icache)不友好
  • 编译生成的Go镜像变大

内联策略:

因为Go函数中内联受到的限制有很多(语言特性,例如interface,defer等,限制了函数的内联)

所以我们要及时调整内联策略,使更多的函数被内联

  • 降低函数调用的开销
  • 增加其他优化的机会:逃逸分析

开销:

  • Go镜像增加~10%
  • 编译时间增加

逃逸分析

逃逸分析是指分析代码中指针的 动态作用域(指针在何处可以被访问)

大致思路:

  • 从对象分配处出发,沿着控制流,观察对象的数据流
  • 若发现指针p在当前作用域s:
    • 作为参数传递给其他函数
    • 传递给全局变量
    • 传递给其他的goroutine
    • 传递给已逃逸的指针指向的对象
  • 则指针p指向的对象逃逸出s,反之则为没有

优化:为逃逸i的对象可以在栈上分配

  • 对象在栈上分配和回收很快:移动sp
  • 减少在heap上的分配,降低GC负担

三、课后个人总结:

这次学习主要了解了有关编译器结构与优化的一些知识

在这一次学习中,我了解到编译器的具体内部结构、静态分析、过程内和过程间的分析等等的一些概念

也学到了函数内联、逃逸分析等多种编译器优化思路

为以后在开发过程中面对此类问题时,提供了思路