寄存器重命名
为什么需要寄存器重命名
在一个程序的不同指令之间, 存在很多相关性, 所谓相关性即是指一条指令的执行需要依赖于另外一条指令的结果, 数据相关性大致可以分为下面几种:
- 写后写(Write after write, WAW), 两条指令将结果写回同一个寄存器中.
- 读后写(Write after read, WAR), 一条指令的目的寄存器是它前面某个指令的源寄存器.
- 写后读(Read after write, RAW), 一条指令的源寄存器来自于它前面某条指令的计算结果.
其实, 三种类型中, 只有 RAW 是真正的相关性, 其余两个都可以通过寄存器重命名来消除.
将原来第二条指令的 R1
重命名为为 R6
, WAW 依赖消除.
将原来第二条指令的 R2
重命名为为 R6
, WAR 依赖消除. (注意,该情况是假想的, 第二条指令比第一条指令先执行)
Bergamot 中的寄存器重命名
寄存器重命名表
在 Bergamot 的设计中, 采用 寄存器重命名表 来记录每一个寄存器的重命名状态.
寄存器 | 是否等待写回 | 数据凭证 | 已写回数据 | 恢复数据 |
---|---|---|---|---|
x1 | false | 0 | 5 | 0 |
x2 | true | 2 | 3 | 7 |
寄存器
列表示实际的寄存器编号, 在 RISC-V 32 规范中, 总共有 32 个整数寄存器, 因此该表总共应有 32 行.
是否等待写回
表示该寄存器是否被重映射, 若被重映射, 则表示当前寄存器需要被之前的某一条指令写回, 并且该指令还在核心执行过程中.
数据凭证
表示寄存器数据的接收凭证, 核心通过数据广播的方式, 广播具有该数据凭证的指令结果, 若寄存器重命名表接收到和当前数据凭证列相同的数据广播, 则表示该重命名的寄存器应该被写回.
已写回数据
表示该寄存器已经写回的数据.
恢复数据
用于分支恢复.
下面通过一个实例来说明 Bergamot 中的 寄存器重命名表的工作方式.
恢复数据字段将在寄存器状态恢复章节讲解, 在本节我们先暂时忽略恢复数据字段的功能.
假设核心顺序的执行三条指令, 并且初始寄存器表的状态为:
(1) x1 = x2 + x3
(2) x1 = x4 * x5
(3) x2 = x1 + x4
寄存器 | 是否等待写回 | 数据凭证 | 已写回数据 |
---|---|---|---|
x1 | false | 0 | 1 |
x2 | false | 0 | 1 |
x3 | false | 0 | 3 |
x4 | false | 0 | 2 |
x5 | false | 0 | 1 |
首先执行第一条指令, 分析源寄存器 x2
和 x3
的状态, 两个源寄存器均已写回, 因此 已写回数据
字段就是当前寄存器的值, 对于第一条指令我们应该计算 1 + 3
的值.
重命名的寄存器永远是目的寄存器, 在第一条指令中, 目的寄存器为 x1
, 为 x1
分配一个数据凭证 1
.
我们将在核心数据广播和前馈网络详细介绍数据凭证, 在这里, 我们可以把它当成是一个指令的唯一 ID 号.
假设指令 1 一直在执行, 现在的寄存器表的状态为:
寄存器 | 是否等待写回 | 数据凭证 | 已写回数据 |
---|---|---|---|
x1 | true | 1 | 1 |
x2 | false | 0 | 1 |
x3 | false | 0 | 3 |
x4 | false | 0 | 2 |
x5 | false | 0 | 1 |
现在考虑重命名指令 2, 考虑源寄存器 x4
和 x5
的状态, 两个源寄存器均已写回, 同样的 已写回数据
字段就是当前寄存器的值, 对于第二条指令我们应该计算 2 * 1
的值.
重命名目的寄存器, 目的寄存器为 x1
, 为 x1
分配一个数据凭证 2
.
假设指令 2 一直在执行, 现在的寄存器表的状态为:
寄存器 | 是否等待写回 | 数据凭证 | 已写回数据 |
---|---|---|---|
x1 | true | 2 | 1 |
x2 | false | 0 | 1 |
x3 | false | 0 | 3 |
x4 | false | 0 | 2 |
x5 | false | 0 | 1 |
注意, 我们丢失了对第一条指令 x1
的数据凭证, 这将意味着我们寄存器重命名表再也无法接收到指令 1 的结果, 但是我们真的还需要指令 1 的结果吗?
接下来执行指令 3, 指令 3 的寄存器包含 x1
, 而 x1
寄存器还在等待写回, 已写回数据
字段的值不再是 x1
的当前值, 对于 x4
则还是使用 已写回数据
字段的值, 在这种情况下, 我们需要计算的是 *2 + 2
, 这里 *2
表示应该监听来自数据凭证 2 的数据广播.
同理, 重命名寄存器 x2
为 3, , 现在的寄存器表的状态为:
寄存器 | 是否等待写回 | 数据凭证 | 已写回数据 |
---|---|---|---|
x1 | true | 2 | 1 |
x2 | true | 3 | 1 |
x3 | false | 0 | 3 |
x4 | false | 0 | 2 |
x5 | false | 0 | 1 |
目前为止, 假设指令 1 执行完毕, 核心发现指令 1 具有数据广播行为, 于是将会向核心发送一条数据广播 *1 = 1 + 3 = 4
, 表示数据凭证 1 对应的值为 4.
但是在寄存器重命名表中没有任何表项监听数据凭证 1, 寄存器重命名表将忽略此广播.
假设指令 2 执行完毕, 核心发现指令 2 同样具有数据广播行为, 于是将会向核心发送一条数据广播 *2 = 2 * 1 = 2
, 表示数据凭证 2 对应的值为 2.
在寄存器重命名表中有表项监听数据凭证 2, 即寄存器 x1
, 此时表项修改为:
寄存器 | 是否等待写回 | 数据凭证 | 已写回数据 |
---|---|---|---|
x1 | false | 2 | 2 |
x2 | true | 3 | 1 |
x3 | false | 0 | 3 |
x4 | false | 0 | 2 |
x5 | false | 0 | 1 |
同时, 指令 3 也在监听数据凭证 2, 即 *2 + 2
, 指令 3 接收到数据 *2 = 2
, 指令 3 现在可以执行计算 2 + 2 = 4
, 并在执行完成后广播 *3 = 4
.
在寄存器重命名表中有表项监听数据凭证 3, 即寄存器 x2
, 此时表项修改为:
寄存器 | 是否等待写回 | 数据凭证 | 已写回数据 |
---|---|---|---|
x1 | false | 2 | 2 |
x2 | false | 3 | 4 |
x3 | false | 0 | 3 |
x4 | false | 0 | 2 |
x5 | false | 0 | 1 |
到这里, 三条指令均执行完毕, 寄存器中的值为:
x1 = 2, x2 = 4, x3 = 3, x4 = 2, x5 = 1
通过该过程, 我们发现:
- 指令 1 和指令 2 现在可以并行执行, 指令 2 不需要等指令 1 写回之后才能执行, 消除了 WAW 依赖性.
- 指令 3 与指令 2 之间存在 RAW 依赖性, 因此需要等待指令 2 广播之后, 才能执行, 寄存器重命名无法消除 RAW 依赖性.
- 只要指令是按照执行顺序依次经过重命名的, 在寄存器重命名表中, 字段
数据凭证
的值可以正确被覆写, 而不会影响程序执行的结果.