跳到主要内容

寄存器重命名

为什么需要寄存器重命名

在一个程序的不同指令之间, 存在很多相关性, 所谓相关性即是指一条指令的执行需要依赖于另外一条指令的结果, 数据相关性大致可以分为下面几种:

  1. 写后写(Write after write, WAW), 两条指令将结果写回同一个寄存器中.
  2. 读后写(Write after read, WAR), 一条指令的目的寄存器是它前面某个指令的源寄存器.
  3. 写后读(Read after write, RAW), 一条指令的源寄存器来自于它前面某条指令的计算结果.

其实, 三种类型中, 只有 RAW 是真正的相关性, 其余两个都可以通过寄存器重命名来消除.

Dismiss WAW

将原来第二条指令的 R1 重命名为为 R6, WAW 依赖消除.

Dismiss WAR

将原来第二条指令的 R2 重命名为为 R6, WAR 依赖消除. (注意,该情况是假想的, 第二条指令比第一条指令先执行)

Bergamot 中的寄存器重命名

寄存器重命名表

在 Bergamot 的设计中, 采用 寄存器重命名表 来记录每一个寄存器的重命名状态.

寄存器是否等待写回数据凭证已写回数据恢复数据
x1false050
x2true237

寄存器 列表示实际的寄存器编号, 在 RISC-V 32 规范中, 总共有 32 个整数寄存器, 因此该表总共应有 32 行.

是否等待写回 表示该寄存器是否被重映射, 若被重映射, 则表示当前寄存器需要被之前的某一条指令写回, 并且该指令还在核心执行过程中.

数据凭证 表示寄存器数据的接收凭证, 核心通过数据广播的方式, 广播具有该数据凭证的指令结果, 若寄存器重命名表接收到和当前数据凭证列相同的数据广播, 则表示该重命名的寄存器应该被写回.

已写回数据 表示该寄存器已经写回的数据.

恢复数据 用于分支恢复.

下面通过一个实例来说明 Bergamot 中的寄存器重命名表的工作方式.

忽略恢复数据

恢复数据字段将在寄存器状态恢复章节讲解, 在本节我们先暂时忽略恢复数据字段的功能.

假设核心顺序的执行三条指令, 并且初始寄存器表的状态为:

(1) x1 = x2 + x3
(2) x1 = x4 * x5
(3) x2 = x1 + x4
寄存器是否等待写回数据凭证已写回数据
x1false01
x2false01
x3false03
x4false02
x5false01

首先执行第一条指令, 分析源寄存器 x2x3 的状态, 两个源寄存器均已写回, 因此 已写回数据 字段就是当前寄存器的值, 对于第一条指令我们应该计算 1 + 3 的值.

重命名的寄存器永远是目的寄存器, 在第一条指令中, 目的寄存器为 x1, 为 x1 分配一个数据凭证 1.

数据凭证

我们将在核心数据广播和前馈网络详细介绍数据凭证, 在这里, 我们可以把它当成是一个指令的唯一 ID 号.

假设指令 1 一直在执行, 现在的寄存器表的状态为:

寄存器是否等待写回数据凭证已写回数据
x1true11
x2false01
x3false03
x4false02
x5false01

现在考虑重命名指令 2, 考虑源寄存器 x4x5 的状态, 两个源寄存器均已写回, 同样的 已写回数据 字段就是当前寄存器的值, 对于第二条指令我们应该计算 2 * 1 的值.

重命名目的寄存器, 目的寄存器为 x1, 为 x1 分配一个数据凭证 2.

假设指令 2 一直在执行, 现在的寄存器表的状态为:

寄存器是否等待写回数据凭证已写回数据
x1true21
x2false01
x3false03
x4false02
x5false01

注意, 我们丢失了对第一条指令 x1 的数据凭证, 这将意味着我们寄存器重命名表再也无法接收到指令 1 的结果, 但是我们真的还需要指令 1 的结果吗?

接下来执行指令 3, 指令 3 的寄存器包含 x1, 而 x1 寄存器还在等待写回, 已写回数据 字段的值不再是 x1 的当前值, 对于 x4 则还是使用 已写回数据 字段的值, 在这种情况下, 我们需要计算的是 *2 + 2, 这里 *2 表示应该监听来自数据凭证 2 的数据广播.

同理, 重命名寄存器 x2 为 3, , 现在的寄存器表的状态为:

寄存器是否等待写回数据凭证已写回数据
x1true21
x2true31
x3false03
x4false02
x5false01

目前为止, 假设指令 1 执行完毕, 核心发现指令 1 具有数据广播行为, 于是将会向核心发送一条数据广播 *1 = 1 + 3 = 4, 表示数据凭证 1 对应的值为 4.

但是在寄存器重命名表中没有任何表项监听数据凭证 1, 寄存器重命名表将忽略此广播.

假设指令 2 执行完毕, 核心发现指令 2 同样具有数据广播行为, 于是将会向核心发送一条数据广播 *2 = 2 * 1 = 2, 表示数据凭证 2 对应的值为 2.

在寄存器重命名表中有表项监听数据凭证 2, 即寄存器 x1, 此时表项修改为:

寄存器是否等待写回数据凭证已写回数据
x1false22
x2true31
x3false03
x4false02
x5false01

同时, 指令 3 也在监听数据凭证 2, 即 *2 + 2, 指令 3 接收到数据 *2 = 2, 指令 3 现在可以执行计算 2 + 2 = 4, 并在执行完成后广播 *3 = 4.

在寄存器重命名表中有表项监听数据凭证 3, 即寄存器 x2, 此时表项修改为:

寄存器是否等待写回数据凭证已写回数据
x1false22
x2false34
x3false03
x4false02
x5false01

到这里, 三条指令均执行完毕, 寄存器中的值为:

x1 = 2, x2 = 4, x3 = 3, x4 = 2, x5 = 1

通过该过程, 我们发现:

  1. 指令 1 和指令 2 现在可以并行执行, 指令 2 不需要等指令 1 写回之后才能执行, 消除了 WAW 依赖性.
  2. 指令 3 与指令 2 之间存在 RAW 依赖性, 因此需要等待指令 2 广播之后, 才能执行, 寄存器重命名无法消除 RAW 依赖性.
  3. 只要指令是按照执行顺序依次经过重命名的, 在寄存器重命名表中, 字段 数据凭证 的值可以正确被覆写, 而不会影响程序执行的结果.

ROB

寄存器重命名分配指如何分配一个重命名后的寄存器, 对于 Bergamot 的寄存器重命名表对应如何填写 数据凭证 字段?

实现寄存器重命名分配有多种方式, 这里不在一一介绍. 在 Bergamot 中, 我们通过 ROB 的方式实现寄存器重命名分配.

ROB 称作重排序缓存队列, 其中按照执行顺序从队头到队尾存放了当前已进入核心还未退休的指令. 在 Bergamot 中, 指令是乱序执行的, 但是指令最终需要按照执行顺序修改核心的状态, ROB 负责记录指令的原始顺序.

Bergamot 的 ROB 是一种循环队列, 每一个队列的位置号都唯一对应的在当前核心中的一条指令, 并且位置号是循环使用的, 由于这两点原因, 直接将 ROB 的位置号作为数据凭证是很合适的行为, 下图是一种寄存器重名表和ROB 的一种可能的状态.

RMT and ROB