IPOInterprocedural Optimization,过程间优化)是跨函数边界分析程序并改写代码的编译器优化方法;当这些优化发生在最终链接阶段时,就是 LTO(链接时优化) 的核心工作内容。

核心问题

普通函数内优化只看一个函数内部,例如删除局部死变量、合并表达式、优化循环。很多重要事实却跨函数存在:

  • 调用者总是把常量 mode = 1 传给被调用函数。
  • 某个函数虽然是外部可见声明,但最终程序里没有任何调用路径能到达它。
  • 某个虚函数调用在最终程序里只有一个可能实现。
  • 小函数调用开销比函数体本身还大,适合内联到调用点。

IPO 要解决的问题是:把程序看成由函数和全局对象组成的图,而不是孤立函数集合。

核心对象

对象作用
call graph调用图;节点是函数,边表示“函数 A 可能调用函数 B”。
points-to / alias 信息描述指针、引用或全局变量可能指向哪些对象。
summary每个函数的摘要,例如读写哪些全局变量、是否有副作用、参数是否影响返回值。
inline cost model判断把被调用函数复制到调用点是否值得的代价模型。
preserved symbol set必须保留的函数或变量集合,来自入口点、导出符号和外部可见引用。

核心机制

IPO 通常先做分析,再做变换。

  1. 构建调用图 G = (F, C)F 是函数集合,C 是调用边集合。直接调用会形成确定边;函数指针、虚调用或动态派发会形成保守的候选边。
  2. 计算函数摘要。例如 read_globals(f)write_globals(f)may_throw(f)return_depends_on_arg0(f)
  3. 在调用图上传播信息。例如如果所有调用点都传入常量 42,被调用函数内部就可能把参数当常量优化。
  4. 按收益和风险做变换:内联、函数克隆、常量传播、去虚化、死函数删除、相同函数合并。
  5. 重新运行局部优化,因为跨过程变换通常会制造新的局部优化机会。

一个最小的数据流表达是:

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 和体积指标验证。

相关术语