高级 #compiler#optimization#scheduling

指令调度

指令调度(Instruction Scheduling)在不改变程序语义的前提下重排指令顺序——让 CPU 流水线更顺畅、缓存更友好、利用指令级并行(ILP)提升性能

指令的顺序不是你想的那样

; 直觉上这样写
mov rax, [mem]    ; 加载——需要 5 个时钟周期
add rax, rbx      ; 要等 rax 加载完成!CPU 会停顿

CPU 在等内存加载完成时,完全可以做别的事——如果编译器重排指令,隐藏这些延迟。

🏫 类比:餐厅备菜 厨师不会等客人点了菜才去洗米——他会提前备菜、提前烧水。等客人点菜时,直接炒就行。指令调度就是”提前准备好,避免空等”。

指令延迟

不同指令的执行时间不同(CPU 需要等待结果就绪):

指令类型延迟(时钟周期)举例
整数加法1add rax, rbx
整数乘法3-5imul rax, rbx
整数除法10-30idiv rcx
内存加载(L1 命中)4-5mov 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 + cd = 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 优化调度

小结

概念要点
指令调度重排指令顺序,隐藏执行延迟
依赖图指令间的数据依赖关系
列表调度按优先级从就绪队列发射指令
软件流水循环中不同迭代的指令交错执行
权衡代码体积、编译时间、硬件差异

为什么先学这个? 指令调度是编译器后端优化的最后一步。至此,你已经学完了编译器的全部主要阶段——从词法分析到代码生成再到优化。接下来可以进入计算机网络或程序语言理论板块继续学习。