“本页使用显式赋值、标签与跳转组成的有限流图。每个基本块有有限条顺序赋值,最后是 goto、双路 if 或 return。表达式由整数常量、变量、加减乘及比较构成;比较返回0或1,if 以非零…”
形式陈述
三地址码(three-address code, TAC)是一类编译器中间表示。其核心指令通常形如
每条普通计算指令至多显式写一个结果位置和两个输入位置,“地址”可以是变量、临时量、常量或抽象存储位置,并不要求是真实机器地址。调用、数组寻址、字段访问和返回常以扩展指令存在,但仍把嵌套计算拆成可命名步骤。
本页用于经典 SSA 的扩展还允许块首的合流伪指令
其中
标签和跳转把线性指令序列分割成基本块;块内除末尾外顺序执行,块末产生控制流图边。TAC 的语义必须说明操作数求值、整数宽度、异常、内存与调用效果。指令“
实现可把同一 TAC 存成 quadruple
直觉
三地址码把一口气完成的表达式拆成流水线上的小工序。源式
中间结果有名字后,公共子表达式消除、常量传播和活跃性分析可以直接谈论哪次定义供哪次使用。复杂控制结构也被还原为标签和跳转,使路径而非语法缩进成为分析对象。
这种拆分没有规定每个源变量只能赋值一次。临时量可以复用,循环计数器也可反复更新;因此 TAC 不等于静态单赋值形式。SSA 常建立在类似 TAC 的指令集之上,再通过版本化名字与合流操作施加额外不变量。把二者混称会掩盖 SSA 构造和消解真正完成的工作。
例子与边界
对 “x:=f(a)+g(b*c)” 且求值顺序固定为从左到右,可得
若
“三地址”也不是严格要求每条指令文本出现三个词。一元负号只有一个输入,复制没有运算,条件跳转可能同时写比较两数。真正约束是把复杂运算分解为有限元的简单原语。相反,一条 “x:=sort(a)” 虽表面只有两个名字,却把任意复杂计算藏进原语;它是否属于所讨论 TAC 取决于指令集规范,而不是计数字符串中的操作数。
内存操作尤其需要分开。
推论与应用
TAC 的细粒度让局部优化能以短窗口描述。若连续出现
当一部分输入要到运行时才提供,统一绑定时间分析先沿赋值依赖求最小动态集合;记忆化流图专门化再把不同静态存储编进不同残余标签。它在纯总整数模型中交付实际代码与逐块对应,动态分支的两种静态值必须分版本保留,不能只在原标签上覆盖一个常量。
公共子表达式消除将“表达式可用”与“结果载体仍有效”分开检查;复制传播在任一端重定义时撤销同值事实,再由死代码消除删除纯总的无用计算。它们在OPT-8同一图任务中实际发出三份中间IR,保留分支路径并把每次运行的加法从5次降为2次。
后端可把一条 TAC 指令扩成多条机器指令,也可把数条融合成一个寻址模式。这里没有一对一对应:目标机器可能只允许寄存器—寄存器运算,常量范围也有限。TAC 提供的是清楚的中间语义接口;指令选择仍需单独证明扩展或融合保持相同观察。
过程间 TAC 还必须决定调用如何表示。若 call 只写一个返回临时量,却省略可能读取和写入的内存、异常后继与被破坏状态,局部公共子表达式消除可能错误跨过调用复用旧 load。显式 effect token、调用摘要或保守 clobber 都可解决接口问题;单纯增加第四个“地址”字段并不能。
参考资料
- Alfred V. Aho, Monica S. Lam, Ravi Sethi, and Jeffrey D. Ullman, Compilers: Principles, Techniques, and Tools, 2nd ed., Pearson, 2007, §6.2.
- Steven S. Muchnick, Advanced Compiler Design and Implementation, Morgan Kaufmann, 1997, §§4.2–4.4.
- Keith D. Cooper and Linda Torczon, Engineering a Compiler, 3rd ed., Morgan Kaufmann, 2023, Chapter 4.