2.1.6 编译优化
现代编译器一般都包含优化器,优化器可以提高生成代码的质量,但会使代码生成过程变得复杂。一般主流的工业化编译器会按照如图2-9所示结构进行设计。
现代编译器设计被分为前端、优化器和后端三大部分,前端包含词法分析、语法分析和语义分析。后端的指令选择、指令调度和寄存器分配实际完成代码生成的工作,而优化器则是对中间代码进行优化操作。实现优化器,必须设计编译器的中间代码表示。中间代码的设计没有固定的标准,一般由编译器设计者自己决定。
图2-9 现代编译器结构
由于中间代码的存在,使得语法制导翻译的结果不再是目标机器的代码,而是中间代码。按照我们自己设计的中间代码形式,上述例子生成的中间代码可能是如下形式:
tmp=var1+100
var2=tmp
即使优化器没有对这段代码进行处理,编译器的后端也能正确地把这段中间代码翻译为目标机制指令。根据指令选择和寄存器分配算法,得到的目标机器指令可能如下:
mov eax,[var1]
add eax,100
mov [var2],eax
编译器后端在指令选择阶段会选择更“合适”的指令实现中间代码的翻译,比如使用“add eax,100”实现tmp=var1+100的翻译。在寄存器分配阶段会尽可能地将变量保存在寄存器内,比如tmp一直保存在eax中。
中间代码的抽象程度一般介于高级语言和目标机器语言之间。良好的中间代码形式使得中间代码生成、目标代码生成以及优化器的实现更加简单。我们设计的优化器实现了常量传播、冗余消除、复写传播和死代码消除等经典的编译优化算法。先通过一个简单的实例说明中间代码优化的工作。
var1=100;
var2=var1+100;
将上述高级语言翻译为中间代码的形式如下:
var1=100
tmp=var1+100
var2=tmp
常量传播优化使编译器在编译期间可以将表达式的结果提前计算出来,因此经过常量传播优化后的中间代码形式如下:
var1=100
tmp=200
var2=200
死代码消除优化会把无效的表达式从中间代码中删除,假如上述代码中只有变量var2在之后会被使用,那么var1和tmp都是无效的计算。因此,消除死代码后,最终的中间代码如下:
var2=200
再经过后端将之翻译为汇编代码如下:
mov [var2],200
由于本书篇幅及作者水平所限,在不能实现所有的编译优化算法的情况下,选择若干经典的优化算法来帮助读者理解优化器的基本工作流程。
至此,我们简单介绍了高级语言源文件转化为目标机器的汇编代码的基本流程。本书设计的编译器支持多文件的编译,因此编译器会为每个源文件单独生成一份汇编文件,然后通过汇编器将它们转换为二进制目标文件。汇编过程中涉及目标机器的指令格式和可执行文件的内容,为了便于理解汇编器的工作流程,需要提前准备与操作系统和硬件相关的知识。