高级 #compiler#optimization#scheduling
指令调度
指令调度(Instruction Scheduling)在不改变程序语义的前提下重排指令顺序——让 CPU 流水线更顺畅、缓存更友好、利用指令级并行(ILP)提升性能
指令的顺序不是你想的那样
; 直觉上这样写
mov rax, [mem] ; 加载——需要 5 个时钟周期
add rax, rbx ; 要等 rax 加载完成!CPU 会停顿
CPU 在等内存加载完成时,完全可以做别的事——如果编译器重排指令,隐藏这些延迟。
🏫 类比:餐厅备菜 厨师不会等客人点了菜才去洗米——他会提前备菜、提前烧水。等客人点菜时,直接炒就行。指令调度就是”提前准备好,避免空等”。
指令延迟
不同指令的执行时间不同(CPU 需要等待结果就绪):
| 指令类型 | 延迟(时钟周期) | 举例 |
|---|---|---|
| 整数加法 | 1 | add rax, rbx |
| 整数乘法 | 3-5 | imul rax, rbx |
| 整数除法 | 10-30 | idiv rcx |
| 内存加载(L1 命中) | 4-5 | mov rax, [rdi] |
| 内存加载(L2 命中) | 10-15 | |
| 内存加载(内存) | 50-200 | |
| 分支错误预测 | 15-25 |
💡 编译器让”等结果”的指令前移,把”不依赖结果”的指令插在”发起加载”和”使用结果”之间——这就是调度。
指令调度的基本方法
依赖图(Dependency Graph)
; 代码
a = b + c
d = e + f
g = a + d
依赖图:
b → (+)
↘
c → (+) → a → (+)
↗
e → (+) → d → ↗
↗
f → (+)
a = b + c和d = e + f没有依赖关系——可以并行或重排g = a + d依赖前两条的结果——必须等它们完成
列表调度(List Scheduling)
算法:
1. 构建指令依赖图
2. 计算每条指令的优先级(关键路径长度)
3. 准备就绪队列(所有依赖已满足的指令)
4. 每次从队列中取最高优先级的指令发射
5. 完成后,解除依赖→新增就绪指令
6. 重复直到所有指令被调度
调度示例
; 未调度 ; 已调度
mov rax, [rdi] ; 加载 mov rax, [rdi] ; 加载
add rax, rbx ; 等 rax mov rcx, [rsi] ; 加载(不依赖 rax!)
mov rcx, [rsi] ; 加载 add rax, rbx ; 现在再等 rax
add rcx, rdx ; 等 rcx add rcx, rdx ; 等 rcx
; (停顿 4 周期) ; (没有停顿!)
软件流水线
对循环体做指令重排——把不同迭代的指令交错执行:
; 原始循环 ; 软件流水
loop: ; 前提:加载迭代 i
load &a[i], rax ; 加载 loop:
mul rax, 2 ; 计算 mul rax, 2 ; 计算迭代 i
store rax, &b[i] ; 存储 store rax, &b[i] ; 存储迭代 i
i += 1 load &a[i+1], rax ; 为下一次迭代预加载
if i < n goto loop i += 1
if i < n goto loop
💡 软件流水让不同迭代的不同阶段同时执行——加载迭代 1 时,计算也在进行。现代编译器(GCC、LLVM)的
-O3会自动做软件流水。
跟踪调度(Trace Scheduling)
对频繁执行的路径(热路径)做激进调度,不担心冷路径:
if (likely_case) {
// 热路径——频繁执行 → 积极优化
// 编译器对这部分的指令做激进重排
// 即使牺牲冷路径的性能也值
} else {
// 冷路径——很少执行 → 不做特殊优化
}
权衡
| 考量 | 说明 |
|---|---|
| 代码膨胀 | 指令调度可能增加代码体积(循环展开、复制等) |
| 编译时间 | 更激进的调度需要更长的编译时间 |
| 硬件差异 | 不同 CPU 的指令延迟不同,调度策略需要调整 |
| 调试困难 | 优化后的代码与源码的对应关系变差 |
# GCC 的指令调度控制
gcc -O2 -fno-schedule-insns # 关闭指令调度
gcc -O2 -fschedule-insns2 # 开启第二遍调度
gcc -mtune=native # 针对当前 CPU 优化调度
小结
| 概念 | 要点 |
|---|---|
| 指令调度 | 重排指令顺序,隐藏执行延迟 |
| 依赖图 | 指令间的数据依赖关系 |
| 列表调度 | 按优先级从就绪队列发射指令 |
| 软件流水 | 循环中不同迭代的指令交错执行 |
| 权衡 | 代码体积、编译时间、硬件差异 |
为什么先学这个? 指令调度是编译器后端优化的最后一步。至此,你已经学完了编译器的全部主要阶段——从词法分析到代码生成再到优化。接下来可以进入计算机网络或程序语言理论板块继续学习。