中间表示(Intermediate Representation, IR)是编译器在源代码和目标机器码之间使用的程序表示;它既比源码更规整,又比机器码保留更多语义,便于分析、优化和代码生成。
核心问题
编译器不能直接靠源码字符串做优化,也不能等到完全变成机器码后才理解高层结构。源码有语法糖、宏、类型系统和语言差异;机器码又丢失了很多类型、控制流和表达式信息。IR 充当中间层,让不同语言前端和不同硬件后端之间可以共享优化器。
典型流水线是:
source code -> AST -> IR -> optimized IR -> machine IR -> assembly/object code不是所有编译器都使用同一层级的 IR。GCC 有 GIMPLE、RTL 等内部表示;LLVM 使用 LLVM IR/bitcode 作为重要中间层。
核心对象
| 对象 | 作用 |
|---|---|
| instruction | IR 指令,例如加法、加载、存储、分支、调用。 |
| basic block | 无中途跳转的连续指令块,只有一个入口和一个出口。 |
| control-flow graph | 基本块之间的跳转图,用于分支、循环和可达性分析。 |
| SSA value | Static Single Assignment 形式中的值,每个虚拟变量只赋值一次。 |
| type / metadata | 类型、调试信息、别名信息、可见性、目标平台属性等辅助事实。 |
| pass | 对 IR 做分析或变换的编译器阶段。 |
核心机制
IR 优化的基本规则是:在保留语言规定的可观察行为前提下,把程序表示变成更容易执行或更小的形式。
可以写成:
P_0 -> P_1 -> ... -> P_n
semantics(P_i) == semantics(P_{i+1})这里 P_i 是第 i 轮 IR 程序,-> 是一次优化 pass,semantics(...) 表示程序在语言标准和目标 ABI 下的可观察行为。等号不是说二进制完全一样,而是说外部可观察结果一致,例如输出、内存可见副作用、异常行为和 ABI 边界不被破坏。
IR 让优化器能做这些事:
- 常量折叠:把
2 + 3改成5。 - 死代码删除:删除结果永远不会被使用且没有副作用的计算。
- 内联:把小函数函数体复制到调用点。
- 循环优化:移动循环不变表达式、展开循环或向量化。
- 别名分析:判断两个指针是否可能指向同一内存位置。
在 LTO(链接时优化) 中,IR 还有一个特殊作用:编译阶段把 IR 放进目标文件,最终链接阶段再读出这些 IR,使优化器跨翻译单元工作。
工程用途
IR 对普通开发者通常是间接可见的,但在这些场景会变得重要:
- 阅读编译器优化结果,例如用 Compiler Explorer 查看 LLVM IR。
- 调试优化误编译或未定义行为。
- 理解 LTO、ThinLTO、PGO、JIT 等编译策略。
- 做静态分析、代码插桩、sanitizer 或 DSL 编译器。
边界与常见坑
- IR 不是源码。宏、注释、某些类型别名和语法结构可能已经被消解。
- IR 不是最终机器码。寄存器分配、指令选择、重定位和 ABI 细节可能还没有完全确定。
- IR 不一定是跨版本稳定格式。GCC LTO bytecode 和 LLVM bitcode 都可能受工具链版本影响,不能把它们当长期兼容的发布格式。
- IR 优化基于语言规则。C/C++ 未定义行为可能让优化器做出看似激进但合法的变换。
- 不同层级 IR 适合不同任务。高层 IR 适合类型和控制流优化,低层 IR 更接近寄存器和指令选择。