10.1 为什么编译器使用 IR
前端理解“意义”,后端理解“机器”。夹在中间的是中间表示(IR):一种为分析、变换和重定向而设计的程序形态,既不为人阅读,也不为某块硬件量身定做。
理论上你完全可以把 AST 直接翻成机器码,玩具编译器就是这么干的。但生产级编译器几乎都不这么做,原因是结构性的,不是风格问题。
正在加载交互实验...
正在加载概念检查...
正在加载本节练习...
N×M 问题
假设支持 种语言、 种目标架构。每种语言直接对每种机器生成,需要 套后端;新增一颗 CPU 就要补 个代码生成器。
共享 IR 把它压扁:每种语言降低一次到 IR( 个前端),每种机器消费一次 IR( 个后端),合计 ,而不是 。
优化收益更夸张:一个优化遍(pass)写在 IR 上,所有语言、所有目标都受益。
正在加载交互实验...
正在加载概念检查...
正在加载本节练习...
好的 IR 长什么样
可用的 IR 应当目标无关、显式、便于检查。它暴露运算、数据流和控制流,但不绑定某块 CPU 的寄存器或 ABI。
正是这种中立性,让常量折叠、死代码消除、公共子表达式消除写一次就服务所有人。一旦把 IR 绑死在某颗芯片上,抽象优势就垮了。
高、中、低三层 IR
真实编译器很少只用一种 IR,而是一摞,越往下越贴近机器:
- 高层 IR 保留偏语言的结构:数组下标、方法调用、边界检查,适合做与源相关的优化。
- 中层 IR 是经典指令列表形态(三地址码、SSA),大多数通用优化都在这层。
- 低层 IR 贴近目标:寄存器、内存寻址、类机器操作,指令选择与寄存器分配都在这层。
LLVM IR、GCC 的 GIMPLE/RTL、JVM/.NET 字节码都是这条谱线上的点。降低(lowering)就是往下走一层,用抽象换控制。
一个具体例子
看 total = price * qty + tax。高层 IR 仍贴近源;降低到三地址中层变成:
code
t1 = price * qty
total = t1 + tax再降低,price、qty 成为寄存器或栈引用,两个运算成为目标的乘加指令。同一句话存在于三个高度,各自服务不同任务。
没有 IR 的代价
没有 IR,丢的不只是复用,更是一个稳定的“思考场所”。优化要问“这个值是常量吗”“这段代码可达吗”,在 AST 上别扭,在裸汇编上更难。显式 IR 给每个优化遍一套统一可读可改的词汇。
正在加载概念检查...