IPO(Interprocedural Optimization,过程间优化)是跨函数边界分析程序并改写代码的编译器优化方法;当这些优化发生在最终链接阶段时,就是 LTO(链接时优化) 的核心工作内容。
核心问题
普通函数内优化只看一个函数内部,例如删除局部死变量、合并表达式、优化循环。很多重要事实却跨函数存在:
- 调用者总是把常量
mode = 1传给被调用函数。 - 某个函数虽然是外部可见声明,但最终程序里没有任何调用路径能到达它。
- 某个虚函数调用在最终程序里只有一个可能实现。
- 小函数调用开销比函数体本身还大,适合内联到调用点。
IPO 要解决的问题是:把程序看成由函数和全局对象组成的图,而不是孤立函数集合。
核心对象
| 对象 | 作用 |
|---|---|
| call graph | 调用图;节点是函数,边表示“函数 A 可能调用函数 B”。 |
| points-to / alias 信息 | 描述指针、引用或全局变量可能指向哪些对象。 |
| summary | 每个函数的摘要,例如读写哪些全局变量、是否有副作用、参数是否影响返回值。 |
| inline cost model | 判断把被调用函数复制到调用点是否值得的代价模型。 |
| preserved symbol set | 必须保留的函数或变量集合,来自入口点、导出符号和外部可见引用。 |
核心机制
IPO 通常先做分析,再做变换。
- 构建调用图
G = (F, C)。F是函数集合,C是调用边集合。直接调用会形成确定边;函数指针、虚调用或动态派发会形成保守的候选边。 - 计算函数摘要。例如
read_globals(f)、write_globals(f)、may_throw(f)、return_depends_on_arg0(f)。 - 在调用图上传播信息。例如如果所有调用点都传入常量
42,被调用函数内部就可能把参数当常量优化。 - 按收益和风险做变换:内联、函数克隆、常量传播、去虚化、死函数删除、相同函数合并。
- 重新运行局部优化,因为跨过程变换通常会制造新的局部优化机会。
一个最小的数据流表达是:
summary(f) = local_fact(f) join facts_from_callees(f)这里 local_fact(f) 是函数 f 内部可直接观察到的事实,facts_from_callees(f) 是从被调用函数传回来的事实,join 是合并运算。合并不是普通字符串拼接,而是按分析目标定义的保守合并:例如“是否可能抛异常”用逻辑或,“可能写哪些全局变量”用集合并集。
工程用途
IPO 常见收益包括:
- 小函数跨文件内联,减少调用开销并打开常量折叠机会。
- 删除不可达函数,减小二进制体积。
- 把“参数总是某个常量”的调用克隆成特化版本。
- 把虚调用或函数指针调用收窄为直接调用,便于进一步内联。
在工程上,IPO 常通过普通优化级别、PGO、LTO(链接时优化) 或编译器专门选项启用。它的收益高度依赖程序结构:边界清晰、导出符号少、调用图稳定时,优化器更容易做出激进但正确的判断。
边界与常见坑
- IPO 不等于 LTO。IPO 是优化类型;LTO 是让 IPO 在链接阶段跨翻译单元发生的一种工程机制。
- IPO 不只等于内联。内联是最直观的 IPO,但死函数删除、去虚化、函数克隆、逃逸分析也属于过程间优化。
- 函数指针、动态加载、反射、插件 ABI 和过宽的符号可见性会让调用图更保守。
- 语言层未定义行为会放大风险。优化器基于语言规则推理,违反规则的代码可能在 IPO 后表现不同。
- IPO 可能增加体积。过度内联会用多个函数体副本换速度,需要用 benchmark 和体积指标验证。