优化是编译器最迷人的部分——它在不改变程序语义的前提下,让程序运行得更快、占用更少资源。下面拆解编译器优化的基础:常量折叠、常量传播、死代码消除、公共子表达式消除和值编号。这些优化看似简单,但它们是所有高级优化的基础。
一、优化的原则与分类
1.1 优化的基本原则
编译器优化必须遵循安全性和正确性原则:
| 原则 | 说明 | 示例 |
|---|---|---|
| 安全性 | 优化不能改变程序的可观察行为 | 不能把 printf("hi") 优化掉 |
| 正确性 | 优化后的程序必须等价于原程序 | x + 0 可以替换为 x |
| 收益性 | 优化应该带来可衡量的收益 | 消除死循环没有收益 |
| 可组合性 | 多个优化的组合应该仍然正确 | 常量传播 + 死代码消除 |
优化的安全性是最重要的原则。一个”让程序快 10 倍但偶尔给出错误结果”的优化是不可接受的。编译器优化中最大的 bug 来源就是优化本身的正确性问题。
1.2 优化的分类
1.3 优化的效果对比
| 优化 | 典型加速比 | 适用场景 | 实现复杂度 |
|---|---|---|---|
| 常量折叠 | 1.0x-1.1x | 宏定义、编译期常量 | 低 |
| 常量传播 | 1.1x-1.3x | 配置参数、条件编译 | 中 |
| 死代码消除 | 1.0x-1.5x | 调试代码、未使用分支 | 低 |
| 公共子表达式消除 | 1.1x-1.3x | 重复计算 | 中 |
| 内联 | 1.2x-2.0x | 小函数调用 | 中 |
| 循环优化 | 1.5x-10x | 计算密集型循环 | 高 |
二、常量折叠(Constant Folding)
2.1 基本原理
常量折叠在编译期计算常量表达式的值,避免运行时重复计算:
// 优化前int x = 3 + 4; // 运行时计算int y = 2 * 3.14; // 运行时计算int z = "hello"[1]; // 运行时索引
// 优化后int x = 7; // 编译期计算int y = 6.28; // 编译期计算int z = 'e'; // 编译期计算2.2 常量折叠的实现
class ConstantFolder: """常量折叠器"""
def fold(self, op, *operands): """尝试折叠一个操作""" # 所有操作数必须是常量 if not all(isinstance(op, Constant) for op in operands): return None
values = [op.value for op in operands]
# 整数运算 if all(isinstance(v, int) for v in values): return self._fold_int_op(op, values)
# 浮点运算 if all(isinstance(v, (int, float)) for v in values): return self._fold_float_op(op, values)
return None
def _fold_int_op(self, op, values): ops = { '+': lambda a, b: a + b, '-': lambda a, b: a - b, '*': lambda a, b: a * b, '/': lambda a, b: a // b if b != 0 else None, '%': lambda a, b: a % b if b != 0 else None, '&': lambda a, b: a & b, '|': lambda a, b: a | b, '^': lambda a, b: a ^ b, '<<': lambda a, b: a << b, '>>': lambda a, b: a >> b, } if op in ops and len(values) == 2: result = ops[op](values[0], values[1]) if result is not None: return Constant(result) return None
def _fold_float_op(self, op, values): ops = { '+': lambda a, b: a + b, '-': lambda a, b: a - b, '*': lambda a, b: a * b, '/': lambda a, b: a / b if b != 0 else None, } if op in ops and len(values) == 2: result = ops[op](float(values[0]), float(values[1])) if result is not None: return Constant(result) return None2.3 常量折叠的边界情况
| 情况 | 处理方式 | 原因 |
|---|---|---|
| 整数溢出 | 不折叠或按语言语义处理 | C 中有符号溢出是 UB |
| 除以零 | 不折叠 | 运行时应该抛出异常 |
| NaN/Inf | 可以折叠 | IEEE 754 定义明确 |
| 浮点精度 | 谨慎折叠 | 不同平台精度可能不同 |
| 字符串连接 | 可以折叠 | Java 的 + 常量字符串 |
浮点常量折叠需要特别小心——不同平台的浮点精度可能不同,可能导致优化后的结果与未优化的结果不同。Java 规范要求严格浮点语义(strictfp),就是为了确保跨平台一致性。
三、常量传播(Constant Propagation)
3.1 基本原理
常量传播将已知的常量值替换使用点,使得更多表达式可以被折叠:
// 优化前int x = 10; // x 是常量int y = x + 5; // x 可以被替换为 10int z = y * 2; // y 可以被替换为 15
// 常量传播后int x = 10;int y = 10 + 5; // x → 10int z = 15 * 2; // y → 15
// 常量折叠后int x = 10;int y = 15;int z = 30;
// 死代码消除后(如果 x, y, z 不再使用)// (空)3.2 常量传播的实现
class ConstantPropagator: """常量传播器"""
def __init__(self, cfg): self.cfg = cfg self.lattice = {} # 变量 → 格值
def propagate(self): """执行常量传播""" # 初始化:所有变量为 ⊤(未知) for block in self.cfg.blocks: for inst in block.instructions: if inst.result: self.lattice[inst.result] = 'TOP'
# 工作表算法 worklist = list(self.cfg.blocks) while worklist: block = worklist.pop(0) changed = self._process_block(block) if changed: for succ in block.successors: if succ not in worklist: worklist.append(succ)
def _process_block(self, block): changed = False for inst in block.instructions: if inst.op == 'const': # 常量定义 old = self.lattice.get(inst.result, 'TOP') new = Constant(inst.operands[0]) if old != new: self.lattice[inst.result] = new changed = True elif inst.op in ('add', 'sub', 'mul', 'div'): # 尝试传播和折叠 operands = [] for op in inst.operands: if isinstance(op, str) and op in self.lattice: val = self.lattice[op] if isinstance(val, Constant): operands.append(val) else: operands.append('TOP') else: operands.append(Constant(op) if isinstance(op, (int, float)) else 'TOP')
if all(isinstance(o, Constant) for o in operands): result = ConstantFolder().fold(inst.op, *operands) if result: old = self.lattice.get(inst.result, 'TOP') if old != result: self.lattice[inst.result] = result changed = True return changed3.3 常量传播的格论基础
常量传播使用**格(Lattice)**来表示变量的可能值:
| 格值 | 含义 | 传播规则 |
|---|---|---|
| ⊤ (Top) | 变量值未知 | 可能是任何值 |
| 常量 c | 变量值确定为 c | 可以替换使用点 |
| ⊥ (Bottom) | 变量值非常量 | 无法传播 |
四、死代码消除(Dead Code Elimination)
4.1 基本原理
死代码消除删除结果未被使用的计算:
// 优化前int foo(int x) { int a = x * x; // a 未使用 → 死代码 int b = x + 1; // b 未使用 → 死代码 return x; // 只有 x 被使用}
// 优化后int foo(int x) { return x;}4.2 死代码消除的实现
class DeadCodeEliminator: """死代码消除器"""
def eliminate(self, cfg): """删除所有死代码""" changed = True while changed: changed = False for block in cfg.blocks: new_insts = [] for inst in block.instructions: if self._is_dead(inst, cfg): changed = True else: new_insts.append(inst) block.instructions = new_insts return cfg
def _is_dead(self, inst, cfg): """判断指令是否是死代码""" # 终止指令永远不是死代码 if inst.op in ('br', 'ret', 'jmp', 'label', 'phi'): return False
# 有副作用的指令不是死代码 if inst.op in ('store', 'call', 'invoke'): return False
# 结果未被使用 → 死代码 if inst.result and not self._is_used(inst.result, cfg): return True
return False
def _is_used(self, var, cfg): """检查变量是否被使用""" for block in cfg.blocks: for inst in block.instructions: if var in inst.operands: return True return False4.3 激进死代码消除(ADCE)
普通 DCE 只能删除”结果未使用”的指令。激进死代码消除(ADCE)还能删除不影响程序输出的指令:
// 普通DCE无法删除(a 被使用)int a = compute_something(); // 有副作用但结果不影响输出int b = a; // b 也不影响输出printf("hello"); // 这才是关键副作用
// ADCE 可以删除 a 和 b 的计算// 因为它们不影响程序的可观察行为4.4 DCE vs ADCE 对比
| 特性 | DCE | ADCE |
|---|---|---|
| 判断标准 | 结果是否被使用 | 是否影响可观察行为 |
| 需要分析 | use-def 链 | 可达性分析 + 副作用分析 |
| 删除能力 | 弱 | 强 |
| 实现复杂度 | 低 | 高 |
| 安全性 | 天然安全 | 需要精确的副作用模型 |
五、公共子表达式消除(CSE)
5.1 基本原理
如果两个表达式计算相同的值,可以只计算一次:
// 优化前int a = b + c;int d = b + c; // 与 a 相同的计算
// 优化后int a = b + c;int d = a; // 复用 a 的值5.2 CSE 的实现
class CSEEliminator: """公共子表达式消除器"""
def eliminate(self, cfg): for block in cfg.blocks: available = {} # 表达式 → 结果变量
new_insts = [] for inst in block.instructions: if inst.op in ('add', 'sub', 'mul', 'div'): # 构造表达式键 key = (inst.op, tuple(sorted(inst.operands)))
if key in available: # 找到公共子表达式,替换为已有结果 inst = IRInst('copy', inst.result, available[key]) else: # 记录新表达式 available[key] = inst.result
new_insts.append(inst) block.instructions = new_insts5.3 局部 CSE vs 全局 CSE
| 特性 | 局部 CSE | 全局 CSE |
|---|---|---|
| 范围 | 单个基本块 | 整个函数 |
| 可用表达式 | 基本块内 | 沿控制流传播 |
| 实现复杂度 | 低 | 高(需要可用表达式数据流分析) |
| 效果 | 有限 | 更好 |
六、值编号(Value Numbering)
6.1 基本原理
值编号为每个不同的计算值分配一个唯一编号。如果两个表达式产生相同的值,它们获得相同的编号:
class ValueNumberer: """值编号器"""
def __init__(self): self.value_map = {} # 表达式 → 值编号 self.number = 0
def get_value_number(self, op, operands): """获取表达式的值编号""" # 规范化操作数(交换律) if op in ('+', '*'): operands = tuple(sorted(operands)) else: operands = tuple(operands)
key = (op, operands)
if key not in self.value_map: self.number += 1 self.value_map[key] = self.number
return self.value_map[key]
def number_block(self, block): """对基本块进行值编号""" var_to_vn = {} # 变量 → 值编号 vn_to_var = {} # 值编号 → 代表变量
for inst in block.instructions: if inst.op in ('add', 'sub', 'mul', 'div'): # 将操作数从变量名转换为值编号 vn_operands = tuple( var_to_vn.get(op, op) for op in inst.operands ) vn = self.get_value_number(inst.op, vn_operands)
if vn in vn_to_var: # 已有相同值的变量,可以替换 inst = IRInst('copy', inst.result, vn_to_var[vn]) else: vn_to_var[vn] = inst.result
var_to_vn[inst.result] = vn6.2 值编号 vs CSE
| 特性 | CSE | 值编号 |
|---|---|---|
| 判断方式 | 语法相同(相同操作+操作数) | 语义相同(代数等价) |
| 交换律 | 不处理 | a+b 和 b+a 视为相同 |
| 分配律 | 不处理 | a*(b+c) 和 a*b+a*c 视为相同 |
| 实现复杂度 | 低 | 中 |
| 效果 | 有限 | 更好 |
七、稀疏条件常量传播(SCCP)
7.1 基本原理
SCCP 是常量传播和死代码消除的结合——它不仅传播常量,还能根据条件分支消除不可达代码:
// 优化前int foo(int x) { if (false) { // 条件为假 return 42; // 不可达代码 } return x + 1;}
// SCCP 后int foo(int x) { return x + 1; // 不可达分支被删除}7.2 SCCP 的格
SCCP 使用更精细的格——每个变量的值可以是:
| 格值 | 含义 |
|---|---|
| ⊤ | 未初始化(还没被分析到) |
| 常量 c | 值确定为 c |
| ⊥ | 非常量(多个可能的值) |
7.3 SCCP 的处理流程
八、优化的组合与迭代
8.1 优化的协同效应
单个优化的效果有限,但组合使用时效果倍增:
// 初始代码int foo(int x) { int a = 10; int b = 20; int c = a + b; // 常量折叠 → c = 30 int d = x * 0; // 乘零消除 → d = 0 int e = d + c; // 常量传播 → e = 0 + 30 = 30 if (d != 0) { // 条件常量传播 → d = 0 → 条件为假 e = 100; // 死代码 } return e; // 常量传播 → return 30}
// 优化后int foo(int x) { return 30;}8.2 优化 Pass 的排列
LLVM 的优化流水线包含数百个 Pass,它们的排列顺序直接决定优化结果:
# 查看 LLVM 的优化流水线opt -passes='default<O2>' -debug-pass-manager input.ll -o /dev/null 2>&1 | head -50
# 典型的优化顺序# 1. mem2reg(SSA 提升)# 2. sccp(稀疏条件常量传播)# 3. dce(死代码消除)# 4. cse(公共子表达式消除)# 5. instcombine(指令合并)# 6. gvn(全局值编号)# 7. licm(循环不变量外提)# 8. indvars(归纳变量简化)# 9. loop-unroll(循环展开)# 10. tailcallelim(尾调用消除)8.3 优化收敛性
优化迭代必须收敛——每轮优化后 IR 应该更小或不变:
| 优化 | 单调性 | 收敛保证 |
|---|---|---|
| 常量折叠 | 是(值从 ⊤ → 常量 → ⊥) | 有 |
| 常量传播 | 是 | 有 |
| 死代码消除 | 是(只删除不增加) | 有 |
| CSE | 是 | 有 |
| 内联 | 否(可能增加代码) | 需要限制 |
九、动手实践
9.1 实验一:手写常量传播 + 死代码消除
# 使用上面的 ConstantPropagator 和 DeadCodeEliminatorsource = """int main() { int a = 10; int b = 20; int c = a + b; int d = c * 0; return c;}"""
# 编译到 IR# ... (使用前面章节的词法分析、语法分析、IR 生成)
# 优化propagator = ConstantPropagator(cfg)propagator.propagate()
eliminator = DeadCodeEliminator()optimized_cfg = eliminator.eliminate(cfg)
# 查看优化后的 IRfor block in optimized_cfg.blocks: for inst in block.instructions: print(inst)9.2 实验二:用 opt 观察优化效果
# 创建测试文件cat > opt_test.c << 'EOF'int foo(int x) { int a = 10; int b = 20; int c = a + b; int d = x * 0; int e = d + c; return e;}EOF
# 生成未优化的 IRclang -emit-llvm -S -O0 opt_test.c -o opt_test_O0.ll
# 逐步优化opt -passes=instcombine opt_test_O0.ll -S -o step1.ll # 指令合并opt -passes=sccp step1.ll -S -o step2.ll # 常量传播opt -passes=dce step2.ll -S -o step3.ll # 死代码消除
# 对比每一步diff opt_test_O0.ll step1.lldiff step1.ll step2.lldiff step2.ll step3.ll
# 一步到位clang -emit-llvm -S -O2 opt_test.c -o opt_test_O2.lldiff step3.ll opt_test_O2.ll9.3 实验三:Compiler Explorer 对比
在 Compiler Explorer 中输入以下代码,对比 -O0 和 -O2 的输出:
int compute(int x) { int a = 100; int b = 200; int c = a + b; int d = x * 0; int e = d + c; if (d > 0) { e = 999; } return e;}十、本章小结
在上一章中,我们看到了 IR 如何成为前端与后端的桥梁,以及 SSA 形式如何让每个变量只有唯一定义——这为优化奠定了基础。现在的问题是:在 SSA 形式的 IR 上,编译器具体能做哪些变换来让程序跑得更快?从最基础的常量折叠和死代码消除开始。
| 概念 | 要点 |
|---|---|
| 优化原则 | 安全性、正确性、收益性、可组合性 |
| 常量折叠 | 编译期计算常量表达式 |
| 常量传播 | 将已知常量替换到使用点 |
| 死代码消除 | 删除结果未使用的计算 |
| 激进DCE | 删除不影响可观察行为的代码 |
| CSE | 消除重复的相同计算 |
| 值编号 | 为语义相同的值分配相同编号 |
| SCCP | 常量传播 + 条件分支消除 |
| 优化组合 | 常量传播 → 折叠 → DCE 迭代直到收敛 |
这一章覆盖了编译器基础优化的关键设计。下一章进入 循环优化,看看编译器如何处理程序中最重要的性能热点——循环。
参考
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






