7.1 中间语言
中间语言在编译过程中扮演着桥梁的角色,连接了源代码和目标代码。它不仅简化了编译器的设计,使得前端和后端的开发可以独立进行,而且也方便了编译器的移植和优化工作。接下来,我们将深入探讨几种常见的中间表示形式,特别是三地址代码,以及它们在编译过程中的应用。
后缀表示
后缀表示,或逆波兰表示,是一种常用的中间表示形式,特别适用于表达式的表示和处理。在后缀表示中,运算符位于其操作数之后,这样的布局消除了表达式中对括号的需求,从而简化了表达式的解析和计算。
例如,表达式 (8-4)+2
在后缀表示中为 8 4 - 2 +
,而 8-(4+2)
的后缀表示则是 8 4 2 + -
。
计算后缀表示
计算后缀表示的一个高效方法是使用栈。通过从左到右扫描后缀表达式,将操作数压入栈中,遇到运算符时,从栈中弹出相应数量的操作数,执行运算,并将结果压回栈中。最终,栈顶元素即为表达式的结果。
三地址代码
三地址代码是另一种常用的中间表示形式,它对高级和低级中间表示都有适用性。三地址代码通过使用简单的指令,每条指令最多有三个操作数,来表示复杂的表达式和控制流程。这种表示形式既可以接近源代码,也可以接近目标机器代码,便于进行各种优化。
优点
- 易于生成和转换:从源代码生成三地址代码相对直接,且易于转换为目标代码。
- 便于优化:三地址代码的结构使得进行代码优化,如常量折叠、死代码消除等,变得更为直接和高效。
选择中间表示
中间表示的选择依赖于编译器的设计目标和特定需求。高级表示(如语法树)适合于进行静态检查和部分优化,而低级表示(如三地址代码)则更适合于机器依赖的优化,如寄存器分配和指令选择。
通过采用适当的中间表示,编译器可以实现更好的目标代码生成和优化,同时保持源代码到目标代码转换过程的灵活性和可维护性。中间代码生成的技术,包括如何从编程语言的各种构造产生三地址代码,是编译器设计中的一个核心环节.
7.1.2 图形表示
图形表示在编译器设计中用于描述源程序的结构和语义,其中语法树和有向无环图(DAG)是两种重要的图形表示形式。这些表示不仅揭示了程序的层次结构,还为代码优化提供了便利。
语法树
语法树是源程序的图形化表示,展示了程序的语法结构。它是从分析树简化而来,去除了一些语法上的细节,直接反映了程序的语义结构。例如,赋值语句 a=(-b+c*d)+c*d
的语法树清晰地展示了运算的顺序和结构。
优点
- 直观:语法树以图形化的方式展现了程序的结构,便于理解程序的构造和操作。
- 适用于静态分析:适合进行类型检查、作用域分析等静态分析任务。
有向无环图(DAG)
DAG是另一种图形表示,用于表示表达式和语句。与语法树相比,DAG通过合并公共子表达式来提高表示的效率,这对于代码优化特别有用。在DAG中,一个公共子表达式只表示一次,即使它在程序中出现多次,从而节省了空间并为公共子表达式的识别提供了直接支持。
优点
- 紧凑:通过合并公共子表达式,DAG提供了比语法树更紧凑的表示。
- 便于优化:直接支持公共子表达式的识别和消除,有利于进行代码优化。
构造语法树和DAG的过程
构造语法树和DAG可以通过语法制导定义实现。对于给定的表达式或语句,通过相应的产生式和语义规则,可以逐步构建出其语法树。如果在构造节点时检查并复用已存在的公共子表达式节点,则可以转换为DAG的构造。
例如,对于赋值语句 a=(-b+c*d)+c*d
,构造其语法树和DAG时,可以通过检查构造节点的过程中是否已经存在相同的节点来决定是否创建新节点或复用现有节点,进而生成对应的DAG,其中公共子表达式 c*d
被合并为单个节点。
总结
图形表示是编译器中间表示的重要形式,其中语法树和DAG各有优势。语法树直观展现了程序结构,适合于静态分析;而DAG通过合并公共子表达式,提供了更紧凑的表示,便于进行代码优化。通过合理应用这些表示,可以有效地支持编译器的分析和优化工作。
7.1.3 三地址代码
三地址代码是编译过程中使用的一种中间表示形式,它以简洁的指令集来描述程序的逻辑,每条指令最多涉及三个操作数。这种表示形式因其结构简单、易于生成和操作,特别适合于进行代码优化和目标代码生成。
基本特性
- 简单且高效:每条指令包含一个操作符和最多三个操作数,这使得三地址代码既易于理解又便于处理。
- 灵活性:支持算术、逻辑、赋值、跳转等多种操作,能够表达丰富的计算和控制流逻辑。
- 优化友好:为代码优化提供了便利,如公共子表达式消除、死代码删除等优化策略可以直接在三地址代码上实施。
- 容易映射到目标代码:三地址代码的结构接近于许多机器语言的指令格式,简化了目标代码的生成过程。
举例
以表达式 a=(-b+c*d)+c*d
为例,其对应的三地址代码序列如下:
t1 = -b
t2 = c * d
t3 = t1 + t2
a = t3 + t2
其中 t1
、t2
、t3
是编译器生成的临时变量,用于存放中间计算结果。
三地址指令的类型
- 赋值指令:包括简单赋值 (
x = y
)、算术运算赋值 (x = y op z
) 和一元运算赋值 (x = op y
)。 - 跳转指令:无条件跳转 (
goto L
) 和条件跳转 (if x relop y goto L
)。 - 过程调用指令:包括参数传递 (
param x
)、调用 (call p, n
) 和返回 (return y
) 指令。 - 数组和指针操作:包括数组访问 (
x = y[i]
) 和指针操作 (x = &y
、x = *y
、*x = y
)。
设计考虑
在设计三地址代码时,选择合适的操作符集合是关键。操作符集应足够大以覆盖源语言的所有操作,同时要尽可能小以简化目标机器上的实现。平衡这一点是中间代码设计中的一个重要考虑。
实现
在编译器内部,三地址代码通常以数据结构(如记录或对象)的形式实现,包含算符域和运算对象域。不同的编译器可能会有不同的实现策略,以适应特定的优化和代码生成需求。
三地址代码的使用极大地简化了编译过程中的许多任务,从而成为现代编译器设计中不可或缺的一部分。
7.1.4 静态单赋值形式 (SSA)
静态单赋值形式(SSA)是中间表示的一种,特别适用于编译器的代码优化阶段。SSA通过确保每个变量在程序中只被赋值一次,简化了变量的生命周期管理,并使得某些类型的优化更加直接和高效。
特点
唯一变量赋值
在SSA中,每个变量的赋值都是唯一的,即每个变量在被重新赋值时会被重命名。这意味着程序中的每个变量实际上都是一个不变量(在其生命周期内不会改变值),从而极大地简化了数据流分析和优化。
φ函数
SSA引入了φ函数来处理变量在不同控制流路径上的多个赋值。φ函数为每个可能的赋值选择一个值,具体取决于程序执行时经过的路径。这使得在控制流合并点能够统一处理来自不同路径的变量值。
优势
- 简化优化:由于每个变量在其生命周期内只有一个赋值,编译器可以更容易地进行死代码消除、常量传播等优化。
- 提高分析效率:SSA简化了数据流分析,如寻找变量的定义-使用链(def-use chains),因为每个变量的定义都是唯一的。
- 便于并行化:SSA形式使得检测数据依赖和实现某些形式的并行化变得更加直接。
示例
考虑以下代码段:
if (flag) x = -1; else x = 1;
y = x * a;
在SSA形式中,它会被转换为:
if (flag) x1 = -1; else x2 = 1;
x3 = φ(x1, x2);
y = x3 * a;
这里,φ(x1, x2)
函数根据执行路径选择x1
或x2
的值给x3
。
应用
SSA形式被广泛应用于现代编译器中,尤其是在代码优化阶段。它为变量赋值和使用提供了清晰的视图,使得优化决策更加准确和高效。通过转换成SSA形式,编译器能够更好地利用程序的数据流信息,进行更深入的优化,如循环不变代码外提、强度削弱等。
总之,SSA不仅优化了编译器的内部处理流程,也为高级代码优化提供了坚实的基础。