编译器死代码消除与代码布局策略总结
一、四大核心 Pass 机制对比
| Pass 简称 | 全称 | 作用域 (Scope) | 处理对象 | 核心逻辑 (Heuristics) | 典型应用场景 |
|---|---|---|---|---|---|
| ADCE | Aggressive DCE | 函数内 (Intra-procedural) | 指令 / 控制流 | 逆向存活分析:假设所有代码都是死的,从“副作用”指令(如 ret, volatile store)倒推,只保留有用的。 | 消除无副作用的死循环;清理复杂的逻辑分支。 |
| GlobalDCE | Global DCE | 模块级 (Inter-procedural) | 全局变量 / 函数 | 根节点可达性:从 Roots (main, 导出函数) 出发遍历调用图,删掉无法触达的符号。 | 清理链接进来的庞大库中未被调用的函数或全局数据。 |
| DeadArgElim | Dead Argument Elimination | 函数签名 (IPO) | 参数 / 返回值 | 签名重写:检查函数体和所有调用点,若参数/返回值未被使用,直接修改函数原型(Prototype)。 | 移除内部函数(static)中冗余的上下文指针或废弃参数。 |
| DSE | Dead Store Elimination | 内存操作 (Memory) | Store 指令 | 覆盖与逃逸分析:若一次写入随后被立即覆盖,或写入对象的生命周期结束且未逃逸,则删除该写入。 | 消除被覆盖的赋值;消除函数返回前对局部变量的无效写入。 |
二、代码布局与重构策略 (Optimization-Friendly Layout)
核心原则:作用域越小,编译器分析越精准(越敢优化)。
1. 链接属性收束 (static is King)
- 策略:除了必须对外导出的 API,所有函数、全局变量一律加上
static(C++ 中使用匿名命名空间)。 - 收益:
- 解锁 DeadArgElim:只有确认为内部函数(Internal Linkage),编译器才敢修改函数签名(删参数/返回值),否则为了 ABI 兼容性必须保留。
- 增强 GlobalDCE:确认为内部符号后,只要当前文件没人用,编译器可 100% 安全删除,无需担心外部链接。
2. 编译单元聚合 (Cohesion)
- 策略:按功能而非技术层级分文件。将“热路径”上的强相关函数(A 高频调用 B)放入同一个
.c/.cpp源文件。 - 收益:
- 辅助 DSE:编译器能直接看到被调函数的实现,确认其是否读取内存。如果确认不读,调用前的
store操作即可被安全消除。 - 促进内联:同文件内联成本最低,分析最透彻。
- 辅助 DSE:编译器能直接看到被调函数的实现,确认其是否读取内存。如果确认不读,调用前的
3. 数据流显式化 (No Globals)
- 策略:拒绝全局变量,使用 Context 结构体 指针在函数间传递状态。
- 收益:
- 辅助 ADCE:减少隐式副作用。全局变量读写被视为副作用,会阻碍 ADCE 删除无用代码;局部变量/结构体则容易追踪生命周期。
- 辅助 Alias Analysis:明确的结构体指针传递,让编译器更容易判断指针是否别名(Aliasing),从而更激进地重排或删除指令。
4. 终极手段 (LTO)
- 策略:若物理布局无法聚合,开启 LTO (Link Time Optimization)。
- 收益:打破文件边界,将多个编译单元合并分析,使跨文件的
static限制失效,实现全局范围的 DCE 和参数消除。
5. 辅助提示 (Attributes)
- 策略:在无法缩减范围时,使用编译器属性显式告知行为(如
__attribute__((pure))、restrict等)。 - 收益:人工担保函数无副作用或指针无别名,强制开启优化。