跳转至

步骤 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 中的相关内容。

在唤醒发射,需要检查 Store 指令时,可以使用如下代码:

xxx.inst == RV32I::SB || xxx.inst == RV32I::SH || xxx.inst == RV32I::SW 

使用以上方式,我们就可以完成保留站的实现了。


最后更新: 2024年5月17日
作者:崔轶锴