编译器优化 Pass 核心总结 (LLVM/IPO)
本文档总结了七种常见的编译器过程间优化(IPO)策略,涵盖其原理、处理机制及最终产生的代码效果。
1. 函数内联类 (Inlining Family)
Inline (标准内联)
- 定义:将被调用函数(Callee)的代码直接嵌入到调用者(Caller)中,替换
call指令。 - 处理机制:基于代价模型 (Cost Model)。编译器计算收益(消除调用开销、暴露后续优化)与成本(代码膨胀导致 I-Cache 未命中)。只有收益 > 成本时才执行。
- 结果:
- 消除了
call/ret指令及参数压栈开销。 - 上下文暴露,解锁常量传播和死代码消除。
- 消除了
Always-inline (强制内联)
- 定义:忽略代价模型,强制将函数内联。
- 处理机制:由开发者通过
__attribute__((always_inline))或__forceinline标记触发。编译器只要不遇到无限递归等硬性障碍,就会无条件内联。 - 结果:
- 确定性:保证关键路径(如底层驱动、汇编封装)无函数调用开销。
- 风险:滥用会导致二进制体积剧烈膨胀。
Partial-inliner (部分内联)
- 定义:只内联函数中经常执行的“热路径”,不内联很少执行的“冷路径”(如异常处理)。
- 处理机制:
- 分析 CFG(控制流图)和分支概率。
- Outlining:将冷路径剥离成一个新的私有辅助函数。
- Inlining:将剩下的热路径内联到调用处。
- 结果:
- 既享受了内联的速度优势,又避免了将庞大的错误处理代码复制多份。
- 提高指令缓存(I-Cache)利用率。
2. 函数合并与参数优化
Mergefunc (函数合并)
- 定义:识别并合并机器码实现完全相同的不同函数(常见于 C++ 模板)。
- 处理机制:计算函数哈希或比较结构。若函数 A 和 B 逻辑一致,保留 A,将 B 实现为跳转到 A 或作为 A 的别名。
- 结果:
- 显著减小二进制文件体积(ICF - Identical Code Folding)。
- 减少指令内存占用。
Argpromotion (参数提升 / Argument Promotion)
- 定义:将“引用传递(指针)”优化为“值传递”。
- 处理机制:如果函数接收一个指针,但在内部:
- 指针未逃逸(未被转义或传给外部)。
- 只读取指针内容(Load),未修改(Store)。 则修改函数签名,直接传递被 Load 出来的值。
- 结果:
- 减少内存访问(Load 指令提前到调用前或消除)。
- 增强别名分析(Alias Analysis),因为值传递没有指针别名问题。
3. 全局与作用域优化
Internalize (内部化)
- 定义:将本应对外可见的全局符号标记为内部私有(Internal/Static)。
- 处理机制:在链接时优化(LTO)阶段,扫描所有模块。如果发现某全局函数或变量没有被外部库引用,将其链接属性改为
Internal。 - 结果:
- 解锁删除:使得 GlobalDCE 可以安全删除未被使用的函数。
- 解锁修改:编译器可以随意修改其签名(如 ArgPromotion)而不必担心破坏 ABI。
Globalopt (全局变量优化)
- 定义:优化或消除不必要的全局变量。
- 处理机制:
- Global to Local:若全局变量只在 main 或特定局部链中使用,降级为局部变量(进而优化进寄存器)。
- Constant Folding:若全局变量只写一次,将其视为常量。
- Heap SRA:若全局指针只用于 main 中的 malloc,优化为栈数组。
- 结果:
- 减少全局内存访问,提升为寄存器操作。
- 直接消除某些变量的存储分配。
4. 优化流水线协作示例
一个典型的优化链条反应:
- Internalize:将函数
foo()标记为私有。 - ArgPromotion:因为
foo()私有了,编译器放心大胆地把它的int*参数改成int值传递。 - GlobalOpt:发现传给
foo()的那个全局变量其实可以变成局部的。 - Inline:将优化后变小的
foo()内联。 - Mergefunc:发现内联后的代码片段与别处重复,进行合并。
- GlobalDCE:清理战场,删除所有不再被引用的旧函数和变量。