编译器设计-代码生成
编译器设计-代码生成
Compiler Design - Code Generation
代码生成可以看作是编译的最后阶段。通过后代码生成,优化过程可以应用到代码上,但这可以看作是代码生成阶段本身的一部分。编译器生成的代码是一些低级编程语言(例如汇编语言)的目标代码。我们已经看到,用高级语言编写的源代码被转换为低级语言,从而生成低级目标代码,该目标代码应至少具有以下属性:
它应该具有源代码的确切含义。
它应该在CPU使用和内存管理方面是高效的。
现在我们将看到如何将中间代码转换为目标对象代码(在本例中是汇编代码)。
有向无环图Directed Acyclic Graph
有向无环图(DAG)是描述基本块结构的工具,有助于看到基本块之间的值流,并提供优化。DAG提供了对基本块的简单转换。DAG可以理解为:
叶节点表示标识符、名称或常量。
内部节点表示运算符。
内部节点还表示表达式的结果或要存储或分配值的标识符/名称。
Example:
t0 = a + b
t1 = t0 + c
d = t0 + t1
窥视孔优化Peephole Optimization
这种优化技术在源代码的本地工作,将其转换为优化的代码。在本地,我们指的是手边代码块的一小部分。这些方法可以应用于中间码和目标码。对一组语句进行分析,并检查是否存在以下可能的优化:
冗余指令消除Redundant instruction elimination
在源代码级别,用户可以执行以下操作:
在编译级,编译器搜索本质上多余的指令。指令的多次加载和存储可能具有相同的含义,即使其中一些指令已被删除。例如:
移动x,R0
移动R0,R1
我们可以删除第一条指令,将句子改写为:
MOV x, R1
无法访问的代码Unreachable code
不可访问代码是程序代码的一部分,由于编程结构的原因,它永远不会被访问。程序员可能不小心编写了一段永远无法访问的代码。
Example:
void add_ten(int x)
{
return x + 10;
printf(“value of x is %d”, x);
}
在这个代码段中,printf语句永远不会被执行,因为程序控件在执行之前返回,因此printf可以被删除。
控制优化流程
代码中存在程序控件在不执行任何重要任务的情况下来回跳转的实例。这些跳跃可以被移除。请考虑以下代码块:
...
MOV R1, R2
GOTO L1
...
L1 : GOTO L2
L2 : INC R1
在这段代码中,标签L1可以在将控件传递给L2时被移除。因此,控件不必跳到L1然后跳到L2,而是可以直接到达L2,如下所示:
...
MOV R1, R2
GOTO L2
...
L2 : INC R1
代数表达式简化Algebraic expression simplification
有时代数表达式可以变得简单。例如,表达式a=a+0可以由自身替换,表达式a=a+1可以简单地由INC a替换。
强度折减Strength reduction
有些操作会消耗更多的时间和空间。它们的“强度”可以通过用其他消耗更少时间和空间但产生相同结果的操作来代替它们而降低。
例如,x*2可以替换为x<1,这只涉及一个左移位。虽然a*a和a2的输出是相同的,但是a2的实现效率要高得多。
访问机器指令Accessing machine instructions
目标机器可以部署更复杂的指令,这些指令能够更有效地执行特定操作。如果目标代码能够直接容纳这些指令,不仅可以提高代码质量,而且可以产生更有效的结果。
代码生成器Code Generator
代码生成器应了解目标计算机的运行时环境及其指令集。代码生成器生成代码时应考虑以下事项:
目标语言:代码生成器必须知道要转换代码的目标语言的性质。这种语言可以帮助一些特定于机器的指令,帮助编译器以更方便的方式生成代码。目标计算机可以具有CISC或RISC处理器体系结构。
IR类型:中间表示有多种形式。它可以是抽象语法树(AST)结构、反向波兰符号或3地址码。
指令选择:代码生成器以中间表示作为输入,并将其转换(映射)为目标机器的指令集。一个表示可以有多种方法(指令)来转换它,因此代码生成器有责任明智地选择适当的指令。
寄存器分配:程序在执行期间有许多值要维护。目标机器的体系结构可能不允许将所有值保存在CPU内存或寄存器中。代码生成器决定在寄存器中保留哪些值。此外,它还决定要用来保存这些值的寄存器。
指令排序:最后,代码生成器决定指令的执行顺序。它为执行指令创建时间表。
描述符 Descriptors
代码生成器在生成代码时必须同时跟踪寄存器(以获取可用性)和地址(值的位置)。对于这两种情况,使用以下两个描述符:
寄存器描述符:寄存器描述符用于通知代码生成器寄存器的可用性。寄存器描述符跟踪存储在每个寄存器中的值。每当在代码生成过程中需要新的寄存器时,都会参考该描述符以获得寄存器的可用性。
地址描述符:程序中使用的名称(标识符)的值在执行时可能存储在不同的位置。地址描述符用于跟踪存储标识符值的内存位置。这些位置可以包括CPU寄存器、堆、栈、存储器或所述位置的组合。
代码生成器会实时更新这两个描述符。对于load语句LD R1,x,代码生成器:
更新值为x和的寄存器描述符R1
更新地址描述符(x)以显示x的一个实例在R1中。
代码生成Code Generation
基本块由三个地址指令序列组成。代码生成器将这些指令序列作为输入。
注意:如果一个名称的值出现在多个位置(寄存器、缓存或内存),则寄存器的值将优先于缓存和主内存。同样,缓存的值将优先于主内存。主存几乎没有任何偏好。
getReg:代码生成器使用getReg函数来确定可用寄存器的状态和名称值的位置。getReg的工作原理如下:
如果变量Y已经在寄存器R中,则使用该寄存器。
否则,如果某个寄存器R可用,它将使用该寄存器。
否则,如果上述两个选项都不可行,它将选择需要最少加载和存储指令数的寄存器。
对于指令x=y OP z,代码生成器可以执行以下操作。假设L是保存y OP z的输出的位置(最好是寄存器):
调用函数getReg,决定L的位置。
通过查询y的地址描述符确定y的当前位置(寄存器或内存)。如果y当前不在寄存器L中,则生成以下指令将y的值复制到L:
MOV y’, L
其中y'表示y的复制值。
使用步骤2中用于y的相同方法确定z的当前位置,并生成以下指令:
OP z’, L
其中z'表示z的复制值。
现在L包含了y OP z的值,这个值是要赋给x的。所以,如果L是寄存器,更新它的描述符以表示它包含了x的值。更新x的描述符以表示它存储在位置L。
如果y和z没有进一步的用途,它们可以返回给系统。
其他的代码结构,如循环和条件语句,则以一般的汇编方式转换为汇编语言。