你好,我是学习委员朱英达。

在“预备知识篇”这个模块,宫老师系统地梳理了编译过程中各个阶段的核心要点,目的就是让我们建立一个编译原理的基础知识体系。那到今天为止,我们就学完了这部分内容,迈出了编译之旅中扎实的第一步。不知道你对这些知识掌握得怎样了?

为了复习,也为了检测我们的学习成果,我根据自己的知识积累和学习情况,整理了一张知识大地图,你可以根据这张地图中标记的七大编译阶段,随时速查常用的编译原理概念和关键算法。

如果你也总结了知识地图,那你可以对照着我这个,给自己一个反馈,看看它们之间有哪些异同点,我们可以在留言区中一起交流和讨论。

不过知识地图的形式,虽然便于你保存、携带、速查,但考虑到图中涉及的概念等内容较多,不方便查看和检索。所以,我还把地图上的知识点,用文字的形式帮你梳理出来了。你可以对照着它,来复习和回顾编译技术的核心概念和算法的知识点,构建自己的知识框架。

你在学习这些预备知识的过程中,可能会发现,宫老师并没有非常深入地讲解编译原理的具体概念、理论和算法。所以,如果你想继续深入学习这些基础知识,可以根据宫老师在每讲最后给出的参考资料,去学习龙书、虎书、鲸书等经典编译原理书籍。当然,你也可以去看看宫老师的第一季专栏课《编译原理之美》。

在我看来,相较于编译方面的教科书而言,《编译原理之美》这门课的优势在于,更加通俗易懂、与时俱进,既可以作为新手的起步指导,也能够帮助已经熟悉编译技术的工程师扩展视野,我很推荐你去学习这门课。所以,我邀请编辑添加了相应的知识点到《编译原理之美》的文章链接,如果你有深入学习的需要,你会很方便地找到它。

好了,一起开始复习吧!

一、词法分析:根据词法规则,把字符串转换为Token

核心概念:正则文法

具体实现:手工构造词法分析器、自动生成词法分析器

手工构造词法分析器

自动生成词法分析器

技术难点

首先,你需要注意,NFA和DFA都有各自的优缺点,以及不同的适用场景。

其次,你需要了解基于正则表达式构造NFA,再去进行模式匹配的算法思路。

二、语法分析:依据语法规则,编写语法分析程序,把 Token 串转化成 AST

核心概念:上下文无关文法

具体实现:自顶向下、自底向上

一种是自顶向下的算法思路,它是指从根节点逐层往下分解,形成最后的AST。

另一种是自底向上的算法思路,它是指从底下先拼凑出AST的一些局部拼图,并逐步组装成一棵完整的AST。

技术难点

首先,你需要掌握LL算法的要点,也就是计算First和Follow集合

其次,你要了解LL算法与LR算法的异同点。

三、语义分析:检查程序是否符合语义规则,并为后续的编译工作收集语义信息

核心概念:上下文相关文法

场景案例

1.控制流检查

像return、break 和continue等语句,都与程序的控制流有关,它们必须符合控制流方面的规则。在 Java 这样的语言中,语义规则会规定:如果返回值不是 void,那么在退出函数体之前,一定要执行一个 return 语句,那么就要检查所有的控制流分支,是否都以 return 语句结尾。

2.闭包分析

很多语言都支持闭包。而要正确地使用闭包,就必须在编译期知道哪些变量是自由变量。这里的自由变量是指在本函数外面定义的变量,但被这个函数中的代码所使用。这样,在运行期,编译器就会用特殊的内存管理机制来管理这些变量。所以,对闭包的分析,也是上下文敏感的。

具体实现:引用消解、符号表、类型系统、属性计算

引用消解

符号表

类型系统

属性计算

四、运行时机制:程序的两种不同的执行模式

通常情况下,程序有两种执行模式:基于物理机、基于虚拟机。

在物理机上运行

举例:C、C++、Golang。

程序运行的原理:基于指令指针寄存器的控制,顺序从内存读取指令执行。

CPU:运行指令的地方。

内存:执行指令相关的另一个硬件。

1.代码区:存放编译完成以后的机器码。
2.静态数据区:保存程序中全局的变量和常量。
3.栈:适合保存生存期比较短的数据,比如函数和方法里的本地变量。

4.堆:适合管理生存期较长的一些数据,这些数据在退出作用域以后也不会消失。

操作系统:除了硬件支撑,程序的运行还需要软件,这就是运行时系统。

在虚拟机上运行

举例:Java、Python、Erlang、Lua。

程序运行的原理:虚拟机是计算机语言的另一种运行时系统。虚拟机上运行的是中间代码,而不是 CPU 可以直接认识的指令。

基于栈的虚拟机:指令在操作数栈的栈顶获取操作数(如JVM、Python虚拟机)。

基于寄存器的虚拟机:类似于物理机,从寄存器取操作数(如Erlang、Lua、Dalvik、Ignition)。

二者的区别:主要在于如何获取指令的操作数。

五、中间代码:运行各种优化算法、代码生成算法的基础

在这门课程中,宫老师主要从用途和层次、解释执行、呈现格式和数据结构等角度来给你讲解IR这一关键概念。如果你想要更深入地了解IR的特点,理解如何生成IR来实现静态编译的语言,你可以去看《编译原理之美》的第242526讲。

IR的用途和层次(从抽象层次的角度来划分)

IR的解释执行

IR的呈现格式

大部分IR没有像源代码和汇编代码那样的书写格式。

IR的数据结构

SSA格式的IR

六、代码分析与优化:优化程序对计算机资源的使用,以提高程序的性能

优化分类

是否与机器有关

优化范围

优化方法

1.把常量提前计算出来

2.用低代价的方法做计算

3.消除重复的计算

4.化零为整,向量计算

5.化整为零,各个优化

6.针对循环,重点优化

7.减少过程调用的开销

8.对控制流做优化

分析方法

  1. 控制流分析(CFA): 基于程序的控制语句(如条件语句、循环语句、分支语句和基本块语句等)进行分析,建立对程序执行过程的理解,从而进一步做出优化。
  2. 数据流分析(DFA):基于数据流分析框架(包含“方向(D)”“值(V)”“转换函数(F)”“初始值(I)”和“交运算(Λ)”5 个元素)等方式,建立对程序中数据变化情况的理解,从而进一步做出优化。
  3. 依赖分析:分析出程序代码的控制依赖(Control Dependency)和数据依赖(Data Dependency)关系。这对指令排序和缓存优化很重要。
  4. 别名分析:在 C、C++ 等可以使用指针的语言中,同一个内存地址可能会有多个别名,因为不同的指针都可能指向同一个地址。编译器需要知道不同变量是否是别名关系,以便决定能否做某些优化。

优化方法的重要性和顺序

重要性

顺序

七、目标代码生成:编译器最后一个阶段的工作,生成针对不同架构的目标代码,也就是生成汇编代码

生成针对不同架构的目标代码

生成目标代码时的优化工作

1.指令选择

2.寄存器分配

3.指令排序

4.窥孔优化

思路:提供一个固定大小的窗口,比如能够容纳10条指令,并检查窗口内的指令,看看是否可以优化;然后往下滑动窗口,再次检查优化机会。

调用约定的影响

整体的处理过程

知识地图

评论