mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1882 字
5 分钟
优化基础:常量折叠与死代码消除
2026-01-01

优化是编译器最迷人的部分——它在不改变程序语义的前提下,让程序运行得更快、占用更少资源。下面拆解编译器优化的基础:常量折叠、常量传播、死代码消除、公共子表达式消除和值编号。这些优化看似简单,但它们是所有高级优化的基础。

一、优化的原则与分类#

1.1 优化的基本原则#

编译器优化必须遵循安全性和正确性原则:

原则说明示例
安全性优化不能改变程序的可观察行为不能把 printf("hi") 优化掉
正确性优化后的程序必须等价于原程序x + 0 可以替换为 x
收益性优化应该带来可衡量的收益消除死循环没有收益
可组合性多个优化的组合应该仍然正确常量传播 + 死代码消除
Warning

优化的安全性是最重要的原则。一个”让程序快 10 倍但偶尔给出错误结果”的优化是不可接受的。编译器优化中最大的 bug 来源就是优化本身的正确性问题。

1.2 优化的分类#

graph TB OPT["编译器优化"] OPT --> LOCAL["局部优化<br/>基本块内"] OPT --> GLOBAL["全局优化<br/>过程内"] OPT --> IPA["过程间优化<br/>跨函数"] LOCAL --> CONST["常量折叠"] LOCAL --> CSE["公共子表达式消除"] LOCAL --> VN["值编号"] GLOBAL --> CP["常量传播"] GLOBAL --> DCE["死代码消除"] GLOBAL --> SCP["稀疏条件常量传播"] IPA --> INL["内联"] IPA --> DEVIRT["去虚化"] IPA --> IPCP["过程间常量传播"] style OPT fill:#e3f2fd,stroke:#1565c0 style LOCAL fill:#e8f5e9,stroke:#2e7d32 style GLOBAL fill:#fff3e0,stroke:#e65100 style IPA fill:#fce4ec,stroke:#c62828

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 None

2.3 常量折叠的边界情况#

情况处理方式原因
整数溢出不折叠或按语言语义处理C 中有符号溢出是 UB
除以零不折叠运行时应该抛出异常
NaN/Inf可以折叠IEEE 754 定义明确
浮点精度谨慎折叠不同平台精度可能不同
字符串连接可以折叠Java 的 + 常量字符串
Note

浮点常量折叠需要特别小心——不同平台的浮点精度可能不同,可能导致优化后的结果与未优化的结果不同。Java 规范要求严格浮点语义(strictfp),就是为了确保跨平台一致性。

三、常量传播(Constant Propagation)#

3.1 基本原理#

常量传播将已知的常量值替换使用点,使得更多表达式可以被折叠:

// 优化前
int x = 10; // x 是常量
int y = x + 5; // x 可以被替换为 10
int z = y * 2; // y 可以被替换为 15
// 常量传播后
int x = 10;
int y = 10 + 5; // x → 10
int 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 changed

3.3 常量传播的格论基础#

常量传播使用**格(Lattice)**来表示变量的可能值:

graph TB TOP["⊤ (未知)"] --> CONST["常量值<br/>42, 3.14, ..."] TOP --> BOT["⊥ (非常量)"] CONST --> BOT style TOP fill:#e3f2fd,stroke:#1565c0 style CONST fill:#e8f5e9,stroke:#2e7d32 style BOT fill:#fce4ec,stroke:#c62828
格值含义传播规则
⊤ (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 False

4.3 激进死代码消除(ADCE)#

普通 DCE 只能删除”结果未使用”的指令。激进死代码消除(ADCE)还能删除不影响程序输出的指令:

// 普通DCE无法删除(a 被使用)
int a = compute_something(); // 有副作用但结果不影响输出
int b = a; // b 也不影响输出
printf("hello"); // 这才是关键副作用
// ADCE 可以删除 a 和 b 的计算
// 因为它们不影响程序的可观察行为

4.4 DCE vs ADCE 对比#

特性DCEADCE
判断标准结果是否被使用是否影响可观察行为
需要分析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_insts

5.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] = vn

6.2 值编号 vs CSE#

特性CSE值编号
判断方式语法相同(相同操作+操作数)语义相同(代数等价)
交换律不处理a+bb+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 的处理流程#

flowchart TB INIT["初始化所有变量为 ⊤"] --> ITER["迭代传播"] ITER --> CHECK{变量值变化?} CHECK -->|是| ITER CHECK -->|否| REPLACE["替换常量"] REPLACE --> DCE["死代码消除"] DCE --> DONE["完成"] style INIT fill:#e3f2fd,stroke:#1565c0 style DONE fill:#e8f5e9,stroke:#2e7d32

八、优化的组合与迭代#

8.1 优化的协同效应#

单个优化的效果有限,但组合使用时效果倍增:

flowchart LR CP["常量传播"] --> CF["常量折叠"] CF --> DCE["死代码消除"] DCE --> CP2["更多常量传播"] CP2 --> CF2["更多常量折叠"] style CP fill:#e3f2fd,stroke:#1565c0 style CF fill:#fff3e0,stroke:#e65100 style DCE fill:#e8f5e9,stroke:#2e7d32
// 初始代码
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 和 DeadCodeEliminator
source = """
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)
# 查看优化后的 IR
for 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
# 生成未优化的 IR
clang -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.ll
diff step1.ll step2.ll
diff step2.ll step3.ll
# 一步到位
clang -emit-llvm -S -O2 opt_test.c -o opt_test_O2.ll
diff step3.ll opt_test_O2.ll

9.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 迭代直到收敛

这一章覆盖了编译器基础优化的关键设计。下一章进入 循环优化,看看编译器如何处理程序中最重要的性能热点——循环。


参考#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

优化基础:常量折叠与死代码消除
https://blog.souloss.com/posts/compiler/optimization-basics/
作者
Souloss
发布于
2026-01-01
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时