步骤 2:保留站
难度:⭐⭐⭐
阅读时间:0.5h
预计代码时间:2h
保留站可以说是 Tomasulo 算法里面最为核心的硬件机构了。每条指令在完成译码之后,均会进入保留站等待唤醒和发射。
通过保留站的唤醒机制,tomasulo 算法实现了指令的乱序执行。配合上 CDB,保留站可以最大化推测执行的效率,提高 CPU 的整体执行速度,减少气泡。
在本步骤中,我们将会在实验框架中完成保留站的功能实现。
唤醒流程
寄存器重命名
在指令进入保留站之后,指令不能够立即开始执行。指令的执行需要准备好对应的操作数。而如果操作数在寄存器中,它有可能正在被其他指令写入,导致操作数未就绪。这样的指令就不能够发射。
在记分牌算法当中,我们给每个寄存器添加了一个就绪状态。如果该指令读写寄存器,则会在进入后端的时候,检查并获取寄存器标记,从而解决对应数据冲突。
而在 Tomasulo 算法当中,我们会给每个寄存器添加一个 ROB ID,表示最后写入这个寄存器的指令,对应的 ROB 表项编号。
如果指令需要寄存器数据,但发现该寄存器被占用,则会获取该寄存器编号。等待对应编号的指令执行完成后,从 CDB 上获取数据。这个通过 CDB 数据激活指令操作数的过程,我们也称其为指令的唤醒。
而如果指令写入寄存器,则会将自己申请到的 ROB 表项编号写入到寄存器上,供后续指令使用。
可以看到,在 Tomasulo 算法的执行流程中,如果发生数据冲突,寄存器的编号会被重命名为 ROB 表项的编号,而不会直接阻塞执行。
唤醒操作
当每条指令执行完成,获得了用于写入寄存器的数据之后,就可以唤醒后续指令了。将 ROB 编号和寄存器数据写入 CDB,与保留站中的有效项目进行比对。
如果有指令需要对应 rob 编号的数据作为自己的操作数的话,则可以提供操作数,唤醒指令。
后端流水线发射
如果一条指令的全部操作数就绪,则可以发射到对应的执行单元当中进行执行。在实验框架中,相同功能流水线只有 1 条,因此不需要考虑同时发射两条指令进入相同后端的情况。
这里要特别注意,Store 指令需要按序发射,保证所有指令正确执行。
保留站的实现
保留站在唤醒指令时,会收到后端传来的如下内容:
struct ROBStatusWritePort {
unsigned result;
bool mispredict, actualTaken;
unsigned jumpTarget;
unsigned robIdx;
};
在保留站的唤醒过程中,只需要使用 result 和 rob 编号这两个字段。
保留站本身会存储指令的发射槽数据,结构如下:
struct IssueSlot {
Instruction inst{};
RegReadBundle readPort1{}, readPort2{};
unsigned robIdx = 0u;
bool busy = false;
};
而 RegReadBundle 中则存储着关键的操作数唤醒数据:
struct RegReadBundle {
bool waitForWakeup;
unsigned robIdx;
unsigned value;
};
而保留站是一个顺序压入,乱序弹出的结构。由于 Store 指令需要由保留站保证执行顺序,因此建议大家在维护保留站信息时,使用压缩队列的方式进行维护。即永远保证保留站内的项目有序,且集中在小标号一侧。下面是一个例子:
| a | b | c | - | 压入指令 a, b, c
| a | c | - | - | 弹出 b
| a | c | d | e | 压入指令 d, e
| a | c | e | - | 弹出 d
| c | e | - | - | 弹出 a
压入指令时,记得要正确写入发射槽中 RegReadBundle 中的相关内容。
提示
在指令插入保留站的时候,指令的源寄存器有如下 3 种可能的状态:
- Regfile 标记寄存器为空闲,此时可以直接读取寄存器堆中的数据。
- Regfile 标记寄存器为占用,ROB Index 对应的指令还未将结果写回 ROB。此时应该让这个源寄存器读口继续等待 CDB 上返回的数据。
- Regfile 标记寄存器为占用,ROB Index 对应的指令已经完成写回但还未提交。此时应该直接从 ROB 中获取寄存器数据。
在唤醒发射,需要检查 Store 指令时,可以使用如下代码:
xxx.inst == RV32I::SB || xxx.inst == RV32I::SH || xxx.inst == RV32I::SW
使用以上方式,我们就可以完成保留站的实现了。