编译原理 第十二章 代码生成Word文件下载.docx
《编译原理 第十二章 代码生成Word文件下载.docx》由会员分享,可在线阅读,更多相关《编译原理 第十二章 代码生成Word文件下载.docx(11页珍藏版)》请在冰豆网上搜索。
又由于目标代码的执行效率在很大程度上依赖于寄存器的使用,所以本章将着重介绍目标代码生成的一些共同问题,如寄存器的分配算法,而不讨论某个特定机器的目标代码生成问题。
对寄存器分配的算法仅限定在一个基本块的范围内,以四元式的中间代码作为输入,以一个称作M的模型机的汇编语言作为输出。
12.2一个计算机模型
假定一个M计算机具有n个通用寄存器为R0,R1,…,Rn-1。
它们既可作为累加器又可作为变址器,如果用'
op'
表示运算符,用'
M'
表示内存单元,用变量名表示该变量所在的单元,'
C'
表示常量,'
*'
表示间址方式存取,指令形式可包含以下四种类型,见表12.1(a)。
表12.1(a)
类型
指令形式
意义(设op是二目运算符)
直接地址型
寄存器型
变址型
间接型
opRi,M
opRi,Rj
opRi,c(Rj)
opRi,*M
opRi,*Rj
opRi,*c(Rj)
(Ri)op(M)
Ri
(Ri)op(Rj)
(Ri)op((Rj)+c)
(Ri)op((M))
(Ri)op((Rj))
(Ri)op(((Rj)+c))
如果op是一目运算符,则"
opRi,M"
的意义为:
op(M)
Ri,其余类型可类推。
以上指令中的运算符(操作码)op包括一般计算机上常见的一些运算符。
我们将某些指令的意义说明如表12.1(b)。
表12.1(b)
指令
意义
LDRi,B
.
STRi,B
JX
CMPA,B
把B单元的内容取到寄存器Ri,即(B)
Ri。
把寄存器Ri的内容存到B单元,即(Ri)
B。
无条件转向X单元。
把A单元和B单元的值进行比较,并根据比较情况把机器内部特征寄存器CT置成相应状态。
CT占两个二进位。
根据A<
B或A=B或A>
B分别置CT为0或1或2。
J<
X
J≤X
J=X
J≠X
J>
J≥X
如CT=0转X单元。
如CT=0或CT=1转X单元。
如CT=1转X单元。
如CT≠1转X单元。
如CT=2转X单元。
如CT=2或CT=1转X单元。
思考问题:
①代码生成器的设计要着重考虑哪些问题?
②决定目标代码的因素有哪些?
12.3一个简单的代码生成器
本节介绍一个简单的代码生成器。
它以四元式的中间代码为输入。
将其转换成第12.2节中所指的M计算机的目标代码为输出,并着重讨论在一个基本块内如何充分利用寄存器提高目标代码的运行效率和给出寄存器分配的一般算法。
12.3.1寄存器分配的原则
①当生成某变量的目标代码时,尽量让变量的值或计算结果保留在寄存器中直到寄存器不够分配时为止,这样引用变量值时可减少对内存的存取次数,以提高运行速度。
②当到基本块出口时,将变量的值存放在内存中,因为一个基本块可能有多个后继结点或多个前驱结点,同一个变量名在不同前驱结点的基本块内出口前存放的R可能不同,或没有定值,所以应在出口前把寄存器的内容放在内存中,这样从基本块外入口的变量值都在内存中。
③对于在一个基本块内后边不再被引用的变量所占用的寄存器应尽早释放,以提高寄存器的利用效率。
对基本块的划分可按基本块的划分算法(见11.2.1)在生成四元式的目标代码时进行,以区分基本块的入口和出口。
12.3.2待用信息链表法
为了在一个基本块内的目标代码中,寄存器得到充分利用,我们需把基本块内还要被引用的变量值尽可能保存在寄存器中,而把基本块内不再被引用的变量所占的寄存器尽早释放。
当由四元式生成相应机器指令时,每翻译一个四元式,如:
A∶=BopC时,则需知道在本基本块内今后还有哪些四元式要对变量A,B,C进行引用。
也就是说若在一个基本块中,变量A在四元式i中被定值,在i后面的四元式j中要引用A值,且从i到j之间没有其它对A的定值点,这时我们称j是四元式i中对变量A的待用信息或称下次引用信息,同时也称A是活跃的,若A被多处引用则可构成待用信息链与活跃信息链。
为了得到在一个基本块内每个变量的待用信息和活跃信息,可以从基本块出口的四元式开始由后向前扫描,对每个变量名建立相应的待用信息链和活跃变量信息链。
考虑到处理的方便,可假定对基本块中的变量在出口处都是活跃的,而对基本块内的临时变量可分为两种情况处理。
a)对于没经过数据流分析且中间代码生成的算法中临时变量不允许在基本块外引用,则临时变量在基本块出口处都认为是不活跃的。
b)如果中间代码生成时的算法允许某些临时变量在基本块外引用时,则假定这些临时变量也是活跃的。
下面介绍对变量待用信息的计算方法。
假设在变量的符号表的记录项中含有待用信息和活跃信息的栏目,其算法步骤如下:
①对各基本块的符号表中的"
待用信息"
栏和"
活跃信息"
栏置初值,即把"
栏置"
非待用"
,对"
栏按在基本块出口处是否为活跃而置成"
活跃"
或"
非活跃"
。
现假定变量都是活跃的,临时变量都是非活跃的。
②从基本块出口到基本块入口由后向前依次处理每个四元式。
对每个四元式i:
A:
=BopC,依次执行下述步骤:
a)把符号表中变量A的待用信息和活跃信息附加到四元式i上。
b)把符号表中变量A的待用信息栏和活跃信息栏分别置为"
和"
非活跃"
由于在i中对A的定值只能在i以后的四元式才能引用,因而对i以前的四元式来说A是不活跃也不可能是待用的。
c)把符号表中B和C的待用信息和活跃信息附加到四元式i上。
d)把符号表中B和C的待用信息栏置为"
i"
,活跃信息栏置为"
注意,以上a)和b),c)和d)的次序不能颠倒。
例若用A,B,C,D表示变量,用T,U,V表示中间变量,有四元式如下:
(1)T∶=A-B
(2)U∶=A-C
(3)V∶=T+U
(4)D∶=V+U
其名字表中的待用信息和活跃信息如表12.2,用'
F'
表示"
和"
,用'
L'
表示活跃,用
(1)、
(2)、(3)、(4)表示四元式序号。
表12.2
f12-3-1.swf
表12.2中"
待用信息链"
与"
活跃信息链"
的每列从左至右为每从后向前扫描一个四元式时相应变量的信息变化情况,空白处为没变化。
待用信息和活跃信息在四元式上的标记如下所示。
(1)T(3)L:
=A
(2)L-BFL
(2)U(3)L:
=AFL-CFL
(3)V(4)L:
=TFF+U(4)L
(4)DFL:
=VFF+UFF
12.3.3代码生成算法
为了在代码生成中能有效地分配寄存器,还需随时掌握各寄存器的使用情况。
用一个数组RVALUE来描述(记录)每个寄存器当前的状况,是处于空闲状态还是被某个或某几个变量占用;
用寄存器Ri的编号值作为数组RVALUE的下标,其数组元素值为变量名(当变量被复写时,则一个寄存器的值可表示多个变量的值);
用数组AVALUE[M]表示变量的存放情况。
因此一个变量的值可能存放在寄存器中或存放在内存中,也可能既在寄存器中又在内存中。
综上所述一个变量的值表示可能有:
RVALUE[Ri]={A,C}表示Ri的现行值是变量A,C的值
AVALUE[A]={A}表示A的值在内存中
AVALUE[A]={Ri,A}表示A的值既在寄存器Ri中又在内存中
AVALUE[A]={Ri}表示变量A的值在寄存器Ri中
有了上述对寄存器和地址的描述,可给出寄存器分配和代码生成的具体算法为:
设GETREG是一个函数过程,它的参数是一个形如i:
A:
=BopC的四元式,每次调用GETREG(i:
=BopC)则返回一个寄存器R,用以存放A的结果值。
对如何给出寄存器R,要用到四元式i上的待用信息,以使寄存器分配合理,对每个四元式的代码生成都要调用函数GETREG。
GETREG分配寄存器的算法为:
①如果B的现行值在某寄存器Ri中,且该寄存器只包含B的值,或者B与A是同一标识符,或B在该四元式后不会再被引用,则可选取Ri作为所需的寄存器R,并转(4)。
②如果有尚未分配的寄存器,则从中选用一个Ri为所需的寄存器R,并转(4)。
③从已分配的寄存器中选取一个Ri作为所需寄存器R,其选择原则为:
占用该寄存器的变量值同时在主存中,或在基本块中引用的位置最远,这样对寄存器Ri所含的变量和变量在主存中的情况必须先做如下调整:
即对RVALUE[Ri]中的每一变量M,如果M不是A且AVALUE[M]不包含M,则需完成以下处理。
a)生成目标代码STRi,M;
即把不是A的变量值由Ri中送入内存中。
b)如果M不是B,则令AVALUE[M]={M},否则,令AVALUE[M]={M,Ri}。
c)删除RVALUE[Ri]中的M。
④给出R,返回。
这样,一旦得到了一个为四元式运算的操作寄存器R,就可以进行代码生成,而当目标代码生成完成后,则又需修改寄存器的使用信息和地址描述信息。
我们可用图12.1和图12.2给出算法的流程图。
图12.1代码生成流程图
f12-3-2.swf
图12.2修改寄存器使用信息和地址描述信息流程图
f12-3-3.swf
在图12.2中的B′=Ri?
和C′=Ri?
是为了判定B和C是否占有寄存器Ri(i=0,…,n),若占有并在四元式i后又不是活跃的则可释放寄存器Ri。
思考问题:
①为什么在代码生成时要考虑充分利用寄存器?
②寄存器分配的原则是什么?
12.4中间语言的选择
由于代码生成的任务是把某种中间语言翻译成某机器的汇编语言或机器语言,因而中间语言在编译过程中起着桥梁作用。
中间语言的选择也成为一个重要的研究课题。
现仅简单介绍中间语言选择的一些问题。
人们期望能找到这样一种中间语言,它既能适用绝大多数高级程序设计语言,也能适用各种计算机硬件的结构,这样只要把各种高级语言都翻译成一种中间语言,而再把中间语言翻译成各种机器的目标语言(汇编语言或机器语言)。
前者称分析程序,后者称代码生成程序,那么对一个新的高级语言只要写出它的分析程序,则可用不同机器的代码生成程序,翻译成各种机器的目标代码,这也就相当在每种机器上都有了这样一种新的语言。
对于一个新的计算机来说,只要再写出中间语言翻译成该机器语言的代码生成程序,那么所有的语言和其应用程序都可应用于这种新机器。
例如:
若用过去传统的方法,有m种语言,要在n种计算机上实现,则需要编写m×
n个编译程序,而若用上述方法选定的通用中间代码实现则只要m+n个编译程序就可实现。
如图12.3和图12.4所示。
图12.3限定几种源语言和目标机无中间语言的编译程序示意图
图12.4限定几种源语言和目标机有中间语言的编译程序示意图
尽管上述的设想是非常可取的,因为当m和n都较大时,为开发编译程序所节省的人力和时间是相当可观的,然而,要设计一种中间语言既满足各种高级程序设计语言的特性又要反映不同计算机的特点,同时还需要达到高级程序设计语言编译程序在各种计算机上实现应有的效率,这是一个相当困难的问题。
人们进一步考虑在限定的条件下解决这样的问题,虽不能理想的解决问题,但在限定的范围内是可以节省人力和时间的,其限定的条件可从以下3种情况考虑。
(1)限定几种高级语言和几种计算机:
这种想法仅是把理想的适应各种高级语言和各种计算机的特点范围缩小而已,它的极小限制是一种高级语言对应一种计算机。
而通常可以把共性较多、特性较接近的高级语言作为几种限定的语言,同样把特点较接近的各种计算机归结为限定的几种计算机。
按这种限定几种高级语言和限定几种计算机所设计的中间语言较容易满足,并且实现后的效率也不难达到。
(2)限定计算机情况下的中间语言:
当限定某种计算机时,而高级语言为多种情况下所设计的中间语言,应能充分反映限定计算机的特点称MSIL(MachineSpecificIntermediatelanguage)。
对这种机器的所有编译程序在分析阶段都生成MSIL,在实现一个编译程序时,尽量把编译过程的大量工作放在代码生成阶段,即MSIL到目标程序的翻译上,以减轻不同语言翻译的分析任务。
因不管多少种高级语言,MSIL到目标程序的代码生成只需做一次即可。
如图12.5所示。
图12.5多种源语言对应一种目标机编译程序的示意图
(3)限定高级语言情况下的中间语言:
这种中间语言是针对某种特定的高级语言设计的,称LSIL(LanguageSpecificIntermediateLanguage),它可以充分反映特定高级语言的特性,只要把这特定的高级语言翻译成中间语言LSIL,然后对不同的计算机只要编写从LSIL到相应目标机的代码生成器,便可实现这特定高级语言的编译程序。
如图12.6所示。
其LSIL的设计要求与具体计算机无关,而LSIL到目标机的代码生成一般来说不难实现。
图12.6一种源语言对应多种目标机时编译程序示意图
以限定高级语言情况下的中间语言,有利于编译程序实现途径的可移植性,如果分析器是用高级语言自身编写,那么编译程序还具有自展功能也为移植带来极大的方便。
这种方法虽然相当成功,但与一种高级语言对应一种计算机所设计的编译程序相比,编译程序的效率还是会有所下降。
(4)假想栈式抽象机
假想栈式抽象机是一种包括基本运算和数据类型的中间语言。
这种抽象机描述了一种或一类高级语言的特性。
例如,由U.Ammann等人提供的可移植的PASCAL编译程序P4,采用的栈式抽象机称SC,对应的中间语言称PCODE。
它的实现是把PASCAL语言翻译成不依赖任何具体计算机,而适合在SC上运行的PCODE代码,然后当需要在某一具体机器上实现PASCAL编译程序时,就用该计算机已有的语言或汇编语言书写一个对抽象机SC及其语言的描述,而描述的最简捷办法是书写对PASCAL目标程序PCODE在SC上的汇编和解释程序,用以代替代码生成过程。
由于它的分析程序是用PASCAL语言自身编写的,所以它的可扩展性和可移植性相当好,若用汇编语言书写解释程序,其效率是可以达到一般实用要求的。
它的最大缺点是由于SC和PCODE不依赖于具体机器,而对其PCODE又用解释执行代替代码生成,因而不能反应近代计算机多变址器的特点,致使变址器不能有效利用,所以这种办法的目标程序执行效率下降。
综上所述,一种合适的中间语言选择是很不容易的,因为既要考虑高级语言的实现模型,又要能体现目标机的特点,再则既要考虑可移植性强又希望移植后的效率尽量不下降。
目前中间代码的形式通常有前缀表示、后缀表示、四元组、三元组、树表示等各种形式。
在实际应用中只好根据具体需要进行选择。
小结:
一个高级程序设计语言编译程序的代码生成部分在编译中起着关键性的作用,而它又与计算机硬件的结构乃致细节紧密相关,这导致了代码生成的可移植性及自动生成算法的研究,无论在理论上还是在实践上都相当困难。
因此,本章介绍的仅是一些一般性的考虑原则问题。
在目前实际应用中大多数高级程序设计语言编译程序的前端用编译程序的构造工具(将在第13章介绍)实现,后端的代码生成按选好的中间代码作为输入,但是中间语言的选择希望既满足各种高级程序设计语言的特性又要反映不同计算机的特点,也是一个相当困难的问题。
实际应用中中间代码的选择如12.4节中给出的限定条件下解决的3种情况。
(1)限定几种高级语言和几种计算机
把共性较多、特性较接近的高级语言作为几种限定的语言,同样把特点较接近的各种计算机归为限定的几种计算机。
(2)在计算机限定情况下的中间语言
限定某种计算机时,而高级语言为多种情况下所设计的中间语言,能充分反映限定计算机的特点。
(3)高级语言限定情况下的中间语言
这种中间语言是针对某种特定的高级语言设计的,它可以充分反映特定高级语言的特性,只要把这特定的高级语言翻译成中间语言LSIL,然后对不同的计算机只要编写从LSIL到相应目标机的代码生成器,便可实现这特定高级语言的编译程序。
其LSIL的设计要求与具体计算机无关。
而LSIL到目标机的代码生成一般来说不难实现,这种编译程序的实现途径可移植性强。
本章小结
【本章小结】
由于代码生成的目标代码取决于具体的机器结构、指令格式、字长及寄存器的个数和种类,并与指令的语义和所用操作系统、存储管理等都密切相关,因此实现非常困难。
课后习题
第12章 习题
第1题:
一个编译程序的代码生成要着重考虑哪些问题?
第2题:
决定目标代码的因素有哪些?
第3题:
为什么在代码生成时要考虑充分利用寄存器?
第4题:
寄存器分配的原则是什么?
问答题答案
问答第1题
解答:
代码生成器的设计要着重考虑目标代码的质量问题,而衡量目标代码的质量主要从占用空间和执行效率两个方面综合考虑。
问答第2题
决定目标代码的因素主要取决于具体的机器结构、指令格式、字长及寄存器的个数和种类,并与指令的语义和所用操作系统、存储管理等都密切相关。
又由于目标代码的执行效率在很大程度上依赖于寄存器的使用,所以目标代码与寄存器的分配算法也有关。
问答第3题
因为当变量值存在寄存器时,引用的变量值可直接从寄存器中取,减少对内存的存取次数,这样便可提高运行速度。
因此如何充分利用寄存器是提高目标代码运行效率的重要途径。
问答第4题
寄存器分配的原则是:
(1)当生成某变量的目标代码时,尽量让变量的值或计算结果保留在寄存器中,直到寄存器不够分配时为止。
(2)当到基本块出口时,将变量的值存放在内存中,因为一个基本块可能有多个后继结点或多个前驱结点,同一个变量名在不同前驱结点的基本块内出口前存放的R可能不同,或没有定值,所以应在出口前把寄存器的内容放在内存中,这样从基本块外入口的变量值都在内存中。
(3)对于在一个基本块内后边不再被引用的变量所占用的寄存器应尽早释放,以提高寄存器的利用效率。
对基本块的划分可按基本块的划分算法(见11.2.1)在生成四元式的目标代码时进行,以区分基本块的入口和出口。