前面提到了经过了词法分析->语法分析->语义分析->中间代码优化,最后的阶段便是在目标机器上运行的目标代码的生成了。目标代码生成阶段的任务是:将此前的中间代码转换成特定机器上的机器语言或汇编语言,这种转换程序便被称为代码生成器。
所以再一次验证了“银弹”理论,如果有问题解决不了,那就加一层抽象层了。所以提高代码移植性的集大成者Java,便是通过引入JVM将应用程序层和具体硬件层隔开。
二、特定机器的优化器和生成器复用:针对特定机器的目标代码生成和优化器的工作极为复杂精深,有一套成功的,当然要尽可能复用它。
由于代码生成阶段的目标代码和具体计算机的结构有关,如指令格式、字长以及寄存器的个数和种类,并与指令的语义和所用操作系统等都密切相关,特别是高级语言的语义功能复杂,并且计算机硬件结构多样性都给代码生成的理论研究带来很大的复杂性,因此实际实现起来是非常困难的。所以难得生成一款后端的代码生成器,当然是想让它可以独立出来,被多次组装参与其他编译器的生产过程。
这种情况被称为定计算机情况:当限定某种计算机时,而高级语言为多种情况下所设计的中间语言,应能充分反映限定计算机的特点称MSIL(Machine Specific Intermediate language)。对这种机器的所有编译程序在分析阶段都生成MSIL,在实现一个编译程序时,尽量把编译过程的大量工作放在代码生成阶段,即MSIL到目标程序的翻译上,以减轻不同语言翻译的分析任务。因不管多少种高级语言,MSIL到目标程序的代码生成只需做一次即可。
当然也正是这种组织特性,让本来是集团作战的编译器生成工作,现如今变得不再是难以企及。同时也让各行各业根据自身需求和侧重点不同,甚至可以定制化自己的行业的专属语言(Domain Specific Language)变得可实现。
衡量最终生成的目标代码的质量无非是从两个方面来进行评估:空间占用和运行效率,这其中涉及到诸多和具体硬件绑定的细节,大多数都属于难度高且并不通用的范畴。而寄存器的使用规则则是少数具有通用的手段,故而可以借分析寄存器分配来分析一下目标代码优化和生成过程。
Q: 为什么在代码生成时要考虑充分利用寄存器?
A: 因为当变量值存在寄存器时,引用的变量值可直接从寄存器中取,减少对内存的存取次数,这样便可提高运行速度。因此如何充分利用寄存器是提高目标代码运行效率的重要途径。
Q: 寄存器分配的原则是什么?
A: (1)逻辑有效范围内尽量保留: 当生成某变量的目标代码时,尽量让变量的值或计算结果保留在寄存器中,直到寄存器不够分配时为止。
(2) 逻辑块出口处,和内存中的源数据同步:当到基本块出口时,将变量的值存放在内存中,因为一个基本块可能有多个后继结点或多个前驱结点,同一个变量名在不同前驱结点的基本块内出口前存放的R可能不同,或没有定值,所以应在出口前把寄存器的内容放在内存中,这样从基本块外入口的变量值都在内存中
(3) 及时释放,提升寄存器使用效率:对于在一个基本块内后边不再被引用的变量所占用的寄存器应尽早释放,以提高寄存器的利用效率。
可以看到在进行缓存淘汰更新时我们采用了LRU策略,LRU策略是属于经典的“线性思维”–即过去常被使用的,在未来也会常被使用。而这里寄存器则不能采用“线性思维”,因为我们看自己的代码都知道,变量的使用虽然在局部范围还算符合“局部性原理”,但是稍微放宽到一定尺度上的变量集使用情况,则发现更多是无序的。所以在进行寄存器分配策略的研究上,只能比较粗暴点,事先将目标代码再次遍历一遍,将变量的使用情况事先整理好,并用所谓“待用信息链”的数据结构来保存每个变量的使用情况。这种策略并非LRU那种后视推导逻辑,而是耍赖的先知般论断逻辑,但是考虑到一旦将目标代码的寄存器使用效率最大化,则无疑是一劳永逸的,故而也是划算的。
变量的待用信息链计算方法
前面根据寄存器的使用原则可以看到,寄存器的分配是以基本块为单位的,因为基本块作为程序流的最小单元,存在着数据同步和异步的问题,故而在进行寄存器分配时,要审核的代码范围只需要涉及到当前基本块即可。
首先为任一变量设置两个信息链:待用信息链和活跃变量信息链。
考虑到处理的方便,可假定对基本块中的变量在出口处都是活跃的,而对基本块内的临时变量可分为两种情况处理。
a) 一般情况下基本块内的临时变量在出口处都认为是不活跃的。
b) 如果中间代码生成时的算法允许某些临时变量在基本块外可以被引用时,则这些临时变量也是活跃的。
在基本块内计算变量的使用信息链(觉得采用栈式更符合这种信息链的更新情况),步骤如下:
① 对各基本块的符号表中的”待用信息”栏和”活跃信息”栏置初值,即把”待用信息”栏置”非待用”,对”活跃信息”栏按在基本块出口处是否为活跃而置成”活跃”或”非活跃”。现假定变量都是活跃的,临时变量都是非活跃的。
② 倒着来从基本块出口到基本块入口由后向前依次处理每个四元式。对每个四元式i:A:=B op C,依次执行下述步骤:
a) 把符号表中变量A的待用信息和活跃信息附加到四元式i上。
b) 把符号表中变量A的待用信息栏和活跃信息栏分别置为 “非待用” 和 “非 活跃”。由于在i中对A的定值只能在i以后的四元式才能引用,因而对i以前的四元式来说A是不活跃也不可能是待用的。
c) 把符号表中B和C的待用信息和活跃信息附加到四元式i上。
d) 把符号表中B和C的待用信息栏置为”i”,活跃信息栏置为”活跃”。
说的麻烦,举个例子就好了:
(1) T∶=A-B
(2) U∶=A-C
(3) V∶=T+U
(4) D∶=V+U
加上信息链条之后,则标记情况如下
(1) T【(3)L】:= A【(2)L】 - B【FL】
(2) U【(3)L】:= A【FL】 - C【FL】
(3) V【(4)L】:= T【FF】 + U【(4)L】
(4) D【FL】:= V【FF】 + U【FF】
这样根据信息链表法,在每次运算到一个表达式时,如果寄存器数目不够,即无空余寄存器可用,则可以遍历一下当前在寄存器中的变量的待用信息链,然后选择接下来最远将被调用的变量释放其所占用寄存器,分配寄存器的算法为:
① 如果B
的现行值在某寄存器Ri中,且该寄存器只包含B
的值,或者B
与A
是同一标识符,或B
在该四元式后不会再被引用,则可选取Ri
作为所需的寄存器R
,并转(4)
;
② 如果有尚未分配的寄存器,则从中选用一个Ri
为所需的寄存器R
,并转(4)
;
③ 从已分配的寄存器中选取一个Ri
作为所需寄存器R
,其选择原则为:占用该寄存器的变量值同时在主存中,或在基本块中引用的位置最远,这样对寄存器Ri
所含的变量和变量在主存中的情况必须先做如下调整:即对RVALUE[Ri]
中的每一变量M
,如果M
不是A
且AVALUE[M]
不包含M
,则需完成以下处理;
a) 生成目标代码ST Ri,M
即把不是A的变量值由Ri中送入内存中;
b) 如果M
不是B
,则令AVALUE[M]={M}
,否则,令AVALUE[M]={M, Ri}
;
c) 删除RVALUE[Ri]
中的M
;
④ 给出R
,返回。
至此可以看到,单纯是寄存器分配便是需要较多数据结构配合和工作时间消耗的,管中窥豹,可以看到代码生成器的一个部件工作量便是很复杂的。