朱娜斐编译原理复习笔记-北京工业大学软件学院

2019-06-13  本文已影响0人  Aurochsy

朱娜婓编译原理学习笔记

说明

Keyword

什么是编译器

什么是编译器

编译器是一个程序,核心功能是把源代码翻译成目标代码

编译器的工作解释

源代码-->编译器的静态计算-->目标程序-->计算机的动态计算-->计算结果

静态计算 是指编译器只根据程序文本静态地分析(如做报错分析、优化分析),而不是真的拿 CPU 去执行

计算机 可能是一个 x86 的物理器(如对应 C 语言),也可能是 JVM java 虚拟机(如对应 java)。

编译器和解释器的比较

解释器也是一类处理程序的程序

区别在于:

编译器简史

第一个编译器是Fortran语言的编译器

该编译器给计算机科学的发展产生了巨大的影响:

编译器内部结构

简述

编译器具有非常模块化的高层结构

编译器可以看做多个阶段构成的“流水线” 结构

编译器规模庞大,拆分模块容易实现和维护

一种没有优化的编译器结构

一种没有优化的编译器结构

一种更复杂的编译器结构

一种更复杂的编译器结构

编译器通常会被划分为两个部分(如下图):

[图片上传失败...(image-188606-1560422078662)]

一个简单的例子

背景一:现在我们设计一个叫做 Sum 的语言,特别简单,仅仅支持两种语法。第一是整形数字 n ,第二是加法表达式 e1 + e2 。举几个例子:

背景二:有一个栈式计算机 Stack (后面会再次讲到),其中有一个操作数栈,只支持两条指令,push nadd 。之所以选择栈式计算机,第一是因为简单,第二是因为 JVM 就是采用了这种形式。其指令的详细解释例子如下:

有了上述两个背景之后,接下来的任务是:将程序 1 + 2 + 3 编译到栈式计算机 Stack 。

第一个阶段是词法分析,先不管其中的原理是什么,总之词法分析会将 1 + 2 + 3 拆分为 1 + 2 + 3 这 5 个部分。(后面会提到词法分析的原理就是用正则表达式匹配)

第二阶段是语法分析,就是将词法分析拆分出来的内容,分析是否满足 Sum 语言的语法要求,即 ne1 + e2 这种语法。

第三个阶段是语法树构造(有时算在语法分析阶段里) ,经过某些计算之后(可以看出是按中序遍历生成了二叉树),得到的抽象语法树如下图:

[图片上传失败...(image-d8b081-1560422078662)]

第四个阶段是根据抽象语法树做代码生成。首先,要满足加法的左结合性,对树进行遍历的时候就要优先遍历左子树,即后序遍历(左右根)

在遍历树节点的过程中,如果遇到整数 n 就生成一条 push n 指令,如果遇到 + 就生成一条 add 指令。

接下来详细看一下这棵树的遍历过程:

词法分析

词法分析简介

简介

从编译器内部结构得知,执行编译的第一个阶段就是词法分析。

词法分析的任务:将字符流转为记号流

字符流即源程序代码,记号流即编译器内部定义的数据结构、编码所识别出的词法单元

词法分析即将源程序代码与编译器内部定义的数据结构相对应

通俗来说,就是将源代码进行最细粒度的拆解,例如上面的例子将 1 + 2 + 3 拆分为 1 + 2 + 3 一样

一个例子

[图片上传失败...(image-58705d-1560422078662)]

如上图,从源代码到记号流(单词流)。

词法分析器会将源程序根据关键字、标识符(变量)、括号、引号、运算符、值(整数、字符串)等这些要素,将其从左到右拆分为若干个记号(单词),其中会忽略空格和换行等。上图中记号流输出的含义:

根据上面的例子,可以总结出 token 其实有固定的形式,就可以定义其数据结构,如下图(本文中高级语言的示例,默认情况下都是 C 语言)

[图片上传失败...(image-61f347-1560422078662)]

理解了例子,定义了数据,接下来就要去探寻词法分析的实现算法,第一,手工构造;第二,自动生成 。

词法分析的手工构造法

手工构造即手写一个词法分析器,例如 GCC LLVM ,优点是利于掌控和优化细节,缺点是工作量大、易出错。手工构造法主要用到“转移图”这种数据结构,下面举两个例子说明。

[图片上传失败...(image-fafc05-1560422078662)]

上图的转移图模型,即可识别逻辑运算符,如 <= < <> >= > 。识别到第一个字符,就继续往下做分支判断,直到返回一个确定的运算符。

图中的 * 即一次回溯,即将当前的这个字符再返回到词法分析器重新进行分析。

例如 >1 ,读到了 1 这个字符时,此时已经确定了运算符是 > ,而当前的 1 并不是运算符的一部分,因此将 1 再重新返回到词法分析器中重新进行分析。

[图片上传失败...(image-8d0828-1560422078662)]

上图是标识符(变量)的转移图模型,以及伪代码。其中 * 即一次回溯,跟上面一样。

关键字(如 class if for 等)是一种特殊的标识符,也满足标识符的规则。

要识别关键字,有两种解决方案:

词法分析的自动生成技术

简介

所谓自动生成技术,就是有这样现成的工具(如 lex flex jlex),输入一些声明式的规范,即可自动生成一个词法分析器。优点当然是简单快速,缺点就是无法控制细节。而这里的“声明式规范”,就是我们常见的正则表达式。下文的内容,就是如何用程序去解析正则表达式,如果你之前看过关于“正则表达式 原理”这类的文章,可能早就有了解了。

先说一下自动生成技术的几个阶段,专业术语后面都有解释:

正则表达式

概念解释

正则表达式是一种数学上的概念,

首先它要有一个完整的字符集 Σ = {...} 要能涵盖程序所有的关键字、变量名、数字、运算符、括号、引号、特殊符号等

然后只有以下几个基本的逻辑:

这就是正则表达式的定义,而现代正则表达式这么多的语法,例如 [a-b] ? + 等,都是后来扩展出的语法糖,即对基本规则的一种简写方式。

正则表达式的形式表示
正则表达式例子
语法糖
语法糖

有限状态自动机 FA

也称“有穷自动机”,是一种数学模型。

简单理解,就是输入一个字符串,输出这个字符串是否满足某个规则(true / false)。

FA(有限状态自动机) 实质上是带有边和节点的有向图。

有限状态自动机

例如有 a + b 这样一个规则,输入“1 + 2 ” 就满足,输 “abc” 这就不满足。

实现原理: 就是先设定几个状态,然后根据输入的字符做状态转移,看最后能否转移到最终的状态。

如下图,输入 abbaabb ,初始状态是 0 ,然后分别输入一个一个的字符,看最后能否将状态转移到 3

[图片上传失败...(image-d00190-1560422078662)]

有限状态自动机 FA 又分为两种:

其实每一个正则表达式,都能对应一个 FA ,因此接下来看一下正则表达式如何生成 FA

从正则表达式 RE 到有限状态自动机 FA

转换过程概述

先将正则表达式生成 NFA ,再将 NFA 生成 DFA 。

这是因为:

从RE生成NFA:Thompson算法。

其基本的逻辑是:

下图是 \varepsilon​ 、c、e1 e2 的NFA:

thompson算法

下图是 e1 | e2 、e1* 的NFA:

Thompson算法2

示例如下:

a(b|c)*

用 Thompson算法的思想画出其NFA的思路:

  1. 首先将该字符串划分为 a (b|c)* 两部分,把(b|c)* 看做一个整体,则可画出 e1 e2 结构的NFA
  2. 然后细分(b|c)*,把b|c 看做一个整体,则可画出 e1 * 结构的NFA,替换掉第一步的e2
  3. 再细分到b、c,画出 e1|e2结构的NFA,替换掉第二步的e1,即可得到最终的NFA
RE转NFA示例
从 NFA 转换 DFA ,子集构造算法。

所谓“子集”就是原来 NFA 的若干状态的集合,通过构造子集,来实现 DFA 。也就是说,此时构造出来的 DFA 就不单单是一个一个的状态节点了,而是一个一个的状态子集。

实际上可分为两步:

所以n0通过a可到的状态集合(边界)为{n1,n2,n3,n4,n6,n9}, 记该集合为q1,n0为集合q0,课类似画出覆盖原NFA所有转换的图

子集构造算法讲解

伪代码如下:

子集构造算法(属于工作表算法)

最后得到的DFA图的结点中,如果其所代表的集合包含了原NFA中的接受状态(终结状态),则该结点也应该化为两个圈的形式表示接受状态。

epsilon闭包的运算-深度优先 epsilon闭包的运算-广度优先
对 DFA 进行最小化的优化: Hopcroft 最小化算法

基本逻辑是,将生成的 DFA 的子集再进行合并,减少节点数量。状态节点越少,占用的空间复杂度越少,提高运算效率。

基于等价类的思想,Hopcroft 算法思路如下:

  1. 等价类的意思是,这个范围内的结点经过转换后依然在这个范围内(可能还无法转换)
  2. 一开始将DFA划分为两个集合N和A(非终止结点集合和终止结点集合)
  3. 将集合不断细分,直到集合内的元素无法再区分为止(为一等价类)

hopcroft 算法示例一:

对于a(b|c)* 的DFA图如下,原来包含q0,q1,q2,q3四个结点:

  1. q0是非终止结点,q1,q2,q3是终止结点,将它们划分为两类

  2. q0无法再分,考虑q1,q2,q3是否可以继续划分:易发现q1,q2,q3之前可以通过c或b进行转换,而c、b转换使得这三个结点仍然处于这个三个结点的组成的集合的区域内,所以这个区域无法再分,将之记为q4

  3. 这样,原来的DFA就简化为下面所示的图

hopcroft算法示例

hopcroft 算法示例二:

  1. 首先将其划分为中非终止结点N和终止结点A两类,N包含q0,q1,q2,q3; A包含q3,q4
  2. 在A中,由于q3,q4不再接受字符输入,状态不变,所以为不可划分的一类; 在N中,q2,q3经过e的输入转换可以脱离原来N的范围到A中,所以划为一类;q0,q1无法被e划分所以又被划为一类
  3. 再看q0,q1,q1经过e转换后能脱离q0,q1组合的集合,而q0不行,所以q0,q1可以进一步拆分
  4. 最后得到{q0},{q1},{q2,q4},{q3,q5}四个结点,优化后得到的的DFA如下图的上面图形所示
hopcorft算法示例二

hopcroft 算法伪代码如下:

hopcroft算法伪代码如下

DFA的代码表示(将图表示为代码形式的数据结构)

概述

DFA(确定的有限状态自动机) 实质上是带有边和节点的有向图,所以可以用表示图的数据结构来表示,不过实际上不同的编译器都对数据结构进行了优化。

可以用多种方式来进行DFA的代码表示:

至于选用哪种方式,取决于对时间和空间的权衡

转移表法

包括两个部分,转移表和驱动代码。

转移表就是一个存储状态和输入字符对应的状态转移情况

驱动代码就是读取输入,然后与转移表内容相比较,看输入的串是否满足DFA的条件。

转移表示例如下:

[图片上传失败...(image-61d03e-1560422078662)]

图中第一列是状态,第一行是字符,例如:

有了以上所有的逻辑,就可以判断一个字符串是否符合一个 RE 的规定,如果符合则可以将字符串拆分为一个一个的记号(token,单词)。

驱动代码如下:

转移表法的驱动代码

大部分编译器对输入的串都是进行最长匹配

最长匹配
跳转表法

跳转表法的好处就是不需要维护转移表法那样大的数组,执行时只需要一个一个代码段执行,所以往往效率较高,但仍不能说跳转表法一定比转移表法好,还是得根据具体的应用场景分析。

对于C语言来说,跳转表可能就是用Label来实现,不同Label的代码段表示不同的状态,转移到某个状态就跳转到对应的代码段,在特定代码段内有显式地在特定条件下跳转到别的 label 的语句。

跳转表代码示意(注意右上角是转移表的示意)

语法分析

语法分析简介

简介

词法分析之后,输出了记号流,然后传递给语法分析,这里主要有两部分工作:

路线图

上下文无关文法 CFG

上文所说的“程序语法的表示”,就是上下文无关文法 CFG(context-free grammar) ,是一个描述语言语法规则的标准的数学工具。

定义

定义

取名为 “上下文无关” 的原因就是因为字符 A总可以被字串 \alpha​ 自由替换,而无需考虑字符 X 出现的上下文。

一个形式语言是上下文无关的,如果它是由上下文无关文法生成的。

示例

[图片上传失败...(image-db91e5-1560422078662)]

上图的左侧就是一个 CFG 的简单示例,其中每一条叫做 “产生式”(即规则),图中的 | 即“或”的意思。简单解释一下这个 CFG 的意思:

[图片上传失败...(image-10a19a-1560422078662)]

上图就用 CFG 描述了一个 *+ 表达式,其中 num 表示一个具体的数字, id 表示标识符(变量),这俩都是终结符,只有E 是非终结符。

结合自然语言理解上下文无关文法

结合自然语言理解上下文无关文法

上面定义英文句子的规则就可以说是一个上下文无关文法。其中,<句子> 被称为开始符号,<主语> <谓语> <代词> 之类的被称为非终结符号,Hegave 之类的被称为终结符号。
归纳起来,一个上下文无关文法G包括四个部分:终结符号,非终结符号,开始符号,产生式。
终结符号就是一门语言中最基本的符号。在程序语言中,基本字、标识符、常数、运算符号等都算终结符号。
非终结符号更像一个抽象的集合,比如“算数表达式”、“赋值句”都可以看做非终结符号。
产生式就是推导规则。

推导

按以下方法可以从文法中推导出它所代表的语言

推导概念

从上面的例子来看,可以根据一个 CFG 推导出若干个句子,例如上图的 CFG 可以推导出 id + num 或者 id * num 或者 (id + num) * num 或者 ……

语法分析就是:给定一个文法 G 和句子 s ,要确定:是否能在 G 的推导结果中,找到 s (即,是否存在对句子的推导)**

如果能推导出来,说明句子 s 符合文法 G 的语法,否则不符合。如下图:

[图片上传失败...(image-717bca-1560422078662)]

最左推导和最右推导

推导方式一般有两种:

乔姆斯基文法体系

分类

文法被分为4类,分别是0型文法、1型文法、2型文法、3型文法。

具体释义和特点如下:

判断方法

四种文法就是规定产生式的左和右边的字符的组成规则不同而已

如何根据产生式左边和右边的特征来进行判断。首先,应该明确,四种文法,从0型到3型,其规则和约定越来越多,限制条件也越来越多,所以,我们判断时可以从最复杂的3型进行判断,依次向下判断,如果不符合3型的,那再看是不是2型的,不是2型的,再看是不是1型的。

先看3型文法(正则文法):

再看2型文法如何判断:

再看1型文法如何判断:

最后是0型文法,只要能描述出来,都属于这个类型,即0型。

文法、上下文无关文法、句子、句型、语言的概念辨析

文法 是描述语言的语法结构的形式规则(即语法规则)。

上下文无关文法 就是文法的一种,它所定义的语法单位是完全上下文无关的

假设G是一个文法,S是开始符号,如果S经过零步或者若干步推出 α,那么称α是一个句型。只包含终结符号的句型是一个句子。文法G产生的所有句子构成一门语言,记为L(G)。

需要注意: 一个文法可以唯一确定一个语言,但是一个语言不一定唯一对应一个文法。

从文法推导出其语言

从文法推导出其语言

分析树和二义性与重写文法

分析树

[图片上传失败...(image-28b199-1560422078662)]

如上图,在文法的推导过程中,可以用树的形式来表示,即分析树

其中, 内部节点都是非终结符,叶子节点都是终结符,中序遍历即可得到最终的句子。PS:到这里,貌似已经看到了最终输出的抽象语法树 AST 的雏形了,其本质就是来源于 CFG 的格式。

不同的推导顺序会得到不同更多语法分析树

上面是对3 + 4 * 5的语法推导,A、B 分别是两种不同的推导顺序(A是最左推导,B不是最右推导,注意上面方式B中,E*E -> E+E * E, 是把左边的第一个E替换成了E+E)

二义性

所谓“二义性”就是指文法的书写会产生一些歧义,例如上图中 *+ 表达式的文法,采用不同的推导顺序得到的结果是不一样的,得到了不同的分析树,虽然它们的叶子结点都相同(即得到的句子相同),但是分析树的含义取决于树的后序遍历,可能分别得出 (3+4)*53+(4*5) ,显然计算结果不同。

为了避免文法的二义性,只能是重写文法,将文法表述的更加详细一些。

重写文法验证例子

对文法的重写要具体问题具体分析,不存在统一的算法。

课程里也没给怎么重写的方法,就是直接给出重写的文法然后推导验证

表达式文法重写示例验证一

该重写后的文法也能产生句子 3+4+5

表达式文法的重写验证例子二

自顶向下分析算法

自顶向下分析思路的算法包含

自顶向下分析算法的思路

上文已经明确了语法分析的定义,即看一个文法 G 是否存在对句子 s 的推导。

自顶向下分析就是其中一个比较典型的算法,其基本逻辑是

因为这是从开始符号出发推出句子,因此称为自顶向下分析,对应于分析树自顶向下的构造顺序

缺点与简单的改进方案

但是,上述过程比较笨重,因为一个 G 能推导出来的句子可能有非常多种,都拿来跟 s 做比较,会发生很多回溯,非常耗时。可以用以下方式进行优化:

按照从左到右的推导顺序,可以最先得到句子 t 的左侧,得到第一个元素就可以开始和s的第一个元素进行匹配了,如果匹配成功则对下一个字符进行匹配,否则该元素回溯,寻找对应产生式(文法的行)的下一个终结符,如果都匹配不上就回溯到父节点...知道遍历完找不到才是匹配失败。

但是,上述优化后的算法,还是可能会有回溯发生,这远远达不到编译器的性能要求。编译器要处理的程序动辄几十万行,必须要求线性时间复杂度的算法,一旦有回溯就会严重影响性能。

自顶向下算法的代码解析

自顶向下算法的代码解析

递归下降分析算法(预测分析算法)

基本思路
递归下降分析算法的特点
一个例子的递归下降分析算法代码
递归下降分析算法代码
递归下降算法的一般代码框架
递归下降算法的一般代码框架

系统地解决"前看符号"发现的多分支问题,设计到FIRST集(开始集)和FOLLOW集(跟随集)的计算,这个将在下面讲到,其实除了系统的解决方法外,我们还可以通过分析文法的特点来写各个非终结符的分析函数,例如下面可以发现E的右部一定是T+T+T...的形式,T则一定是F* F * F * ...的形式,就可以通过设计合适的分析函数来判断句子是否符合该文法

通过对文法的分析来解决前看符号会遇到的多分支选择的问题

LL(1) 分析算法

简介

递归下降分析算法适合于手工编码,而 LL(1) 分析算法适用于语法分析的自动生成。

词法分析器的自动生成需要输入的声明式规范是正则表达式,语法分析器的自动生成需要输入的声明式规范是CFG(上下文无关文法)

所谓“LL(1)”,是指:从左(L)向右读入程序,最左(L)推导,采用 1 个前看符号。分析高效,也是线性时间复杂度。

其基本思想是 —— 表驱动的算法,如下图。

LL(1)分析表

第一列都是非终结符,第一行都是终结符,行列交叉点表示对应的产生式序号(注意要是在非终止符及其FIRST集中的元素的交叉点填写)。

[图片上传失败...(image-2cac5e-1560422078662)]

回顾之前讲过的自顶向下分析算法,最大的问题就在于去盲目推导,盲目匹配出句子,然后再去和目标句子 s 做对比,对比出错就要回溯,时间复杂度非常高。因此,就需要在推导过程中就需要做分析预测,就可以从参考这个分析表。从分析表中,通过预测输入能得到产生式的序号,就知道接下来要匹配哪个产生式了,就不需要回溯了。

如何得到分析表--FIRST集

FIRST集定义及示例

FIRST(N)= 从非终结符N开始推导,得出的全部句子中的开头的所有可能的终结符集合。如下面的FIRST(N)={s,t,g,w}

注意其定义也是递归的,如果N的右部有非终结符的话,还要接着递归知道终结符

注意下面给出的计算公式只是近似公式,后面还要提到Numball集和

FIRST定义
First集代码及求解示例

下图中第一列是非终止符,第一行是循环进行的迭代次数,列是FIRST集(开始符号集)

其中第二次循环S(S-> N V N)的变化是被赋予了上一次循环中得到的N的FIRST集,因为N是不可穿过的,所以V对S没有影响(这个后面会讲到,如果N的开始集中包含空串的画则N就是可穿过的)

First集代码及求解示例
FIRST表指导填写LL(1)分析表
FIRST表指导填写LL(1)分析表
LL(1) 文法的定义/LL(1)文法的冲突

如果文法的分析表每个表项只有一个元素的话,那么就将其称为LL(1)文法

如果表项中有多余一个元素的话,则称之为LL(1)分析表中更多冲突

LL(1)文法的冲突
一般条件下LL(1)分析表的构造-引入NULLLABEL、跟随集FOLLOW
image
NULLLABEL定义
1560287982409
NULLBEL示例
1560288137767
FIRST集合的完整计算公式(考虑了NULLBEL在内)
1560288252970
FOLLOW集的计算算法

对于一个非终结符的FOLLOW集,其元素即其产生式中每个非终结符后面跟的第一个终结符

其实本质就是扫描每一条产生式,然后按照每一条产生式中所包含的字符的排列顺序,求解每个非终结符后面跟的第一个终结符,然后把这个终结符并入该非终结符的跟随集中。循环这个过程。

FIRST_S集的计算就是在FIRST的基础上,将NULLABEL集的问题考虑在内

自底向上算法

基本思想

最右推导:每一步替换最右边的非终结符,最右推导称为规范推导

实质上是最右推导的逆过程

自底向上分析的基本思想

LR(0) 分析算法

上文主要讲自顶向下的分析算法,而 LR 分析算法是自底向上的思路,但是输入、输出都是一样的。

算法思想

画出有限状态自动机

状态和字符结合 移进

规约,右侧弹出去,左侧弹进来

LR(0)算法分析思想 LR(0)分析表)

SLR算法--LR算法的改进

SLR算法与LR(0)算法的区别 SLR算法示例

抽象语法树AST

[图片上传失败...(image-76af8b-1560422078662)]

如上图,先看下抽象语法树 AST 和高级语言如何对应,根据代码对比一下,应该不难理解。其中,if 的最左侧节点是判断条件,中间节点是成功分支,右侧节点是 else 分支。

再来回顾一下上文讲的 CFG 的分析树(上文有示意图),它详细编码了句子的推导过程,并且包含了很多无关信息(非终结符),会占用很多存储空间,会增加算法的空间和时间复杂度。如果能把这些“无关信息”给去掉,只留下运算符,数字,标识符等和程序相关的信息,就构成了抽象语法树 AST ,如下图。

[图片上传失败...(image-93b432-1560422078662)]

既然是一棵树,那么就是一个标准的数据结构,各个类型的节点的数据结构,也就可以固定了。如下图

[图片上传失败...(image-2f5363-1560422078662)]

AST 是编译器中非常重要的数据结构,因为它是编译器前端和后端的接口形式。后续的过程仅仅依赖于 AST ,不会再依赖于前面的源码或者字符集。因此,一旦生成了 AST ,前面的源码就会被丢弃。因此,AST 中要有很详细的信息,不仅仅是本课程中讲的这个简单的树。例如,AST 要存储当前程序的文件、行、列,这样在语法报错时才能准确的给出具体的错误位置。

语义分析

语法分析输出 AST ,然后对 AST 进行语义分析(有些教材也会叫做 “类型检查” 或者 “上下文相关分析” 等名字)。注意,程序如果能通过了语义分析这个阶段,那再往后就不应该出现任何语法错误,除非是编译器自己的 bug 。

主要任务

上文中的语法分析用到的是 CFG 即上下文无关的语法,即不依赖于上下文。例如 C 语言中 printf(n); 不符合语法,而 print("%d", n); 就符合语法,但是其中的 n 变量是否在上文已经定义了,语法分析是不知道的。

因此,语义分析是在 AST 基础上,结合上下文来分析:

语义规则和实现

例如表达式的类型检查,定义一个类型检查函数,传入 AST 的某个表达式的节点,然后判断最后返回的类型。如果类型检查错误,就报错。如下图。

[图片上传失败...(image-f8e536-1560422078662)]

符号表

上下文相关分析,就涉及到上下文信息的记录和读取,这些信息就被记录到符号表中,一个非常核心的数据结构。符号表用来存储程序中的变量相关信息(表的信息要足够丰富):

其数据结构最简单的可以使用一个 key-val 的字典来实现,例如 { key1: {…}, key2: {…}, key3: {…} } 。但是编译器要处理的程序规模可能非常庞大,因此这个数据结构必须要合理规划。在实际工程中,高效的查询方式可以有一下选择:

变量都有“作用域”的概念,不同作用域可以有相同的变量名。符号表处理作用域的方式:

[图片上传失败...(image-615737-1560422078662)]

代码生成

经过语义分析的 AST ,即可用来做代码生成,即生成最终的机器(物理机或者虚拟机)代码。注意,这里直接从 AST 到目标代码,是一种最简单的编译器模型,暂时忽略了优化的部分。优化过程下文会详细解说。

主要工作

代码生成是把源程序翻译成“目标机器”(可能是真实的机器,也可能是虚拟机)上的代码,而且要保证和源程序的“等价性”(重要!!!)。主要的任务是:

接下来通过两个示例来看代码生成的过程:

Stack 栈计算机代码生成技术

70 年代有栈计算机的物理机,但是今天已经退出了历史舞台,因为执行效率太低。但是这里还要研究 Stack ,一来是因为在 Stack 上代码生成比较简单,二来是很多虚拟机是这样设计的,例如 JVM 。

[图片上传失败...(image-c53112-1560422078662)]

上图就是一个 Stack 的原型图,简单解释一下图中各个部分。

PS:以上这几条指令,就是 java 字节码的一个子集。真实的 java 字节码有 200+ 个。

[图片上传失败...(image-b62eb6-1560422078662)]

上图就是高级语言到最终的 Stack 计算机机器语言的对应,展示了最终的输入和输出。至于代码生成如何实现,在文章一开始的“Sum 语言 + Stack”的例子中这部分已经写的比较详细,就不再赘述了,翻看上文吧。

REG 寄存器计算机的代码生成技术

这种机器类型是基于寄存器架构,所有操作都在寄存器完成,执行效率非常高(因为寄存器访问速度是内存访问速度的百倍),访存都通过 loadstore 指令(RISC 指令集特点)。

[图片上传失败...(image-4336da-1560422078662)]

上图就是寄存器计算机的原型图,解释一下图中各个部分。

[图片上传失败...(image-69743d-1560422078662)]

上图就是高级语言和目标代码的对应关系。图中有对应的 AST ,对这棵树进行后续遍历(先左、再右、最后根),每遍历一个节点都会对应到右侧的一行指令。

最后,实际的物理机器上不可能有无限多的寄存器,因此要确定哪些变量被用于寄存器?哪些变量被“溢出”放在内存?—— 这个问题是另外一个编译器的重要部分:编译器分配。如何进行编译器分配,这个问题会在下文介绍。

中间表示

中间表示是一个统称,有很多种表示形式,AST 就是其中之一。上文提到,从 AST 直接生成目标代码是比较原始的编译技术,现代编译器中往往会在编译器的“后端”进行各种各样的代码优化,不同的优化形式就需要不同的表示形式。

[图片上传失败...(image-e26a41-1560422078662)]

常见的中间代码形式:

三地址码

所谓“三地址码”,即一个指令有一个运算符,最多有三个操作数。这样就使得每一条指令都比较简单,易于和机器语言对应。

[图片上传失败...(image-f5e86e-1560422078662)]

上图就是一个高级语言和三地址码的对应关系(虽然三地址码是通过 AST 生成的,已经和源代码没有关系)。从图中可以看出三地址码的特点:

控制流图

三地址码是一种线性的表示方式,这就没法通过它来分析和确定流程。例如上图中,哪些指令会跳转到 L_1L_2 ?并不好确定。控制流图是一种更加精细的三地址码(本质上还是三地址码),将程序中的各个控制流块都表示了出来,如下图。

[图片上传失败...(image-828b69-1560422078662)]

控制流图就是一个有向图 G = (V, E) ,其中节点 V 表示程序的基本块,边 E 表示基本块之间的跳转关系。生成控制流图的目的有很多,但都是为了做代码优化,例如:

数据流分析

所谓“数据流分析”,就是通过静态的观察程序(并不执行)来判断其中的变量和数据的一些变化,例如某程序第五行的 x 变量的值会有几种可能?

[图片上传失败...(image-32689a-1560422078662)]

如上图,通过控制流图,既可以判断一个变量 y 的赋值可能性。如果 y 能编译器识别为一个固定的值,直接 a = 3 并且把一开始的 y = 3 删掉。这就是一个优化过程。

但是这仅仅是静态的分析,程序并未执行,因此如果 y 在一个逻辑分支中出现,就不好预估其准确结果,但是至少能预估一个结果集(称为“保守信息”)。如果能将这个结果集做到最小,和执行的结果越接近,就越好优化。这仍然是编译器现在的一个热门话题。

类似数据流分析的还有“到达定义分析”,即分析一个变量是如何一步一步的被定义和使用的,原理和目的基本一致,这里不再赘述。

活性分析

上文中提到 REG 机器假设有无限个寄存器,但实际情况不是。因此需要寄存器分配 —— 即用到活性分析。所谓“活性分析”,即分析变量的活跃区间(可以理解为声明周期)然后来做寄存器的分配。

[图片上传失败...(image-e42d64-1560422078662)]

如上图,三个变量,只有一个寄存器,该如何分配?答案是:计算出每个变量的活跃区间,即可共享寄存器。寄存器分配,就依赖于变量的活动区间数据。如下图:

[图片上传失败...(image-d59a1e-1560422078662)]

代码优化

现代生产环境下的编译器,代码优化是其重要工作之一,而且一直在不断的持续优化中。

几点说明

代码优化的目的是让目标程序更精简、更快速、更节省空间、更节能(所谓的多快好省),当然在不改变语义的前提下 —— 这些应该都比较好理解。但是还有几点关于优化的需要重点说明一下。

[图片上传失败...(image-ca8235-1560422078662)]

前端优化

即对 AST 进行优化,下面列举几个例子来说明。

第一,常量折叠。静态计算,可以在数字类型和 bool 类型进行优化,例如:

第二,代数化简。利用代数的恒等式,进行优化,例如:

第三,死代码(不可达)代码优化,例如

中间表示上的优化

如常量传播、拷贝传播,在上文讲数据流分析的时候已经写过,不再赘述。

附:北京工业大学软件学院2019年编译原理考点划分与考题回忆(朱娜斐老师元年版)

考题回忆

选择题

简答题

分析与计算题

考试题型

老师画的考点(by@杰哥)

考点1
考点2

参考资料

[1] 编译原理学习笔记

[2] 编译原理 中科大华宝健_bilibili(带目录索引)

[3] 编译原理 中科大华宝健_bilibili(完整版)

[4] 朱娜斐老师的PPT

上一篇 下一篇

猜你喜欢

热点阅读