RAT (Register Alias Table) 是 CPU 前端的寄存器别名表。

SSA (Static Single Assignment) 是静态单赋值,一种编译器内部的中间代码表示形式。

今天突然想到这两个东西的一个共同之处,就是它们用类似的方法做到了假依赖消除,区别就是 SSA 是静态的,RAT 是动态的。

具体来说,针对一个变量/寄存器有新的赋值的时候,不改旧值,而是找个新的来装新值(SSA 用新名字接,RAT 用新物理寄存器接,旧的那份都先留着,直到没人用再丢)。

真依赖 & 假依赖

在之前的博客中也说过一些通过消除串行依赖来优化性能的相关内容。依赖就比较好理解了,下一条指令的执行依赖于上一条指令的结果。依赖也有真依赖和假依赖:

  • 真依赖:不管外界情况怎么样,不能交换它们的执行顺序。
  • 假依赖:可以通过增加寄存器/变量的数量来破除,达到并行。也就是本质上来说假依赖是名字上的依赖,不是数据依赖。

举例说明:

RAW 示例

1
2
3
I1: mov eax, 5       ; 写 eax
; ↓ 数据流(I2 真的需要 I1 的结果)
I2: add ebx, eax ; 读 eax

结果:这是真实的数据依赖(True Dependency),CPU 必须先执行 I1 再执行 I2,无法乱序或并行。

WAR 示例

1
2
3
I1: mov eax, ebx     ; 读 ebx
; ↓ 仅仅是名字冲突(I1 读旧值,I2 写新值)
I2: mov ebx, ecx ; 写 ebx

结果:这是假依赖(False Dependency)。
CPU 通过 RAT (Register Alias Table) 将 ebx 重命名为不同的物理寄存器(例如 p10 和 p12),使得 I1 读取 p10,I2 写入 p12。这样两条指令就可以并行执行,互不干扰。

这里提一嘴 RAT 的另外一个好处,就是像 MOV RAX, RCX 这种指令,在重命名阶段就可以直接通过修改映射表来解决,不会进入发射阶段。

上面我们可以看到 CPU 前端用 RAT 来重命名,消除假依赖,它的目的主要是提高微指令的单核并行度。那 SSA 呢?SSA 这种形式更多的是为了方便后续的其他优化,举例说明:

1. 常量传播/折叠变得简单

1
2
3
4
5
非SSA:           SSA:
x = 5 x1 = 5
y = x + 3 y1 = x1 + 3 // 编译器立即知道x1=5
x = 10 x2 = 10
z = x * 2 z1 = x2 * 2 // x2和x1是不同变量!

2. 死代码消除更高效(DCE.cpp)

  • 如果 SSA 变量没有 use,直接删除其定义
  • 不需要复杂的活跃性分析

3. GVN 优化直接受益(GVN.cpp:9-14)

1
2
3
4
5
6
// GVN消除完全冗余的指令
// SSA形式下,相同的Expression自动有相同的操作数
// 示例:
a1 = b + c
...
a2 = b + c // GVN发现这与a1完全相同,用a1替换

4. LICM 外提条件判断简单(LICM.cpp:30-32)

1
2
3
4
// 非SSA需要检查:循环内是否重新赋值?
// SSA形式:如果变量定义在循环外,它就是不变的!
loop:
y = x1 + 5 // x1定义在外面→直接外提