中间表示Intermediate Representation, IR)是编译器在源代码和目标机器码之间使用的程序表示;它既比源码更规整,又比机器码保留更多语义,便于分析、优化和代码生成。

核心问题

编译器不能直接靠源码字符串做优化,也不能等到完全变成机器码后才理解高层结构。源码有语法糖、宏、类型系统和语言差异;机器码又丢失了很多类型、控制流和表达式信息。IR 充当中间层,让不同语言前端和不同硬件后端之间可以共享优化器。

典型流水线是:

source code -> AST -> IR -> optimized IR -> machine IR -> assembly/object code

不是所有编译器都使用同一层级的 IR。GCC 有 GIMPLE、RTL 等内部表示;LLVM 使用 LLVM IR/bitcode 作为重要中间层。

核心对象

对象作用
instructionIR 指令,例如加法、加载、存储、分支、调用。
basic block无中途跳转的连续指令块,只有一个入口和一个出口。
control-flow graph基本块之间的跳转图,用于分支、循环和可达性分析。
SSA valueStatic 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 更接近寄存器和指令选择。

相关术语