步骤 3:后端发射和提交
难度:⭐⭐⭐
阅读时间:1h
预计代码时间:2h + 2h 调试
发射流程
每条指令的执行从前端取指开始,经过前端处理,中端译码之后,来到指令发射单元。
在指令发射单元要处理以下两件事情:
-
选择执行单元,进入对应的后端流水线
Tomasulo 算法中,后端流水线根据功能类型分为了若干种。正常来讲,每条指令要么不使用后端流水线(如实验中的 EXIT,以及实际环境中的
ecall, ebreak
指令等),要么恰好使用唯一 1 条后端功能流水线。若干相同的后端执行流水线可以被绑定成一个大的流水线组。而 Tomasulo 为了每个不同功能的后端流水线都配备了一个保留站,用于暂存指令,等待唤醒执行。而在发射单元中,每条指令就要根据自己功能的不同,选择进入不同的保留站等待唤醒。
-
申请重排序缓冲区(ROB)的位置
原本的 Tomasulo 算法在没有 ROB 的情况下不能够保证指令按序执行,这为部分修改处理器状态的指令的执行带来了难度。而 ROB 为每条指令的执行增加了一个新的提交阶段。
我们知道,指令在前端取指译码的过程中,相对顺序不会改变。因此我们可以利用并维护这个顺序,在提交的时候使用,来对所有乱序执行的指令进行重排序,保证部分功能性指令的正确执行。
因此,为了维护指令顺序,每条指令在离开前端的时候,需要申请一个 ROB 的位置,用来进行指令重排。
提交流程
当后端流水线完成指令执行之后,会根据指令携带的 rob 编号,将对应的结果写入 rob,同时将 rob 表项置为就绪状态。
当 rob 发现队首就绪之后,就可以开始指令的提交。
提交时,指令需要完成对 CPU 状态的修改,包含以下操作:
-
Store 指令写入内存,弹出 Store Buffer
-
Load 指令检查有效性,弹出 Load Buffer
-
指令写入寄存器
-
跳转
实现
发射时,可以使用 getFUType 函数来获取指令的执行单元类型。根据不同的类型,将指令压入保留站和 ROB 即可。记得检查是否可以插入。
而提交时,需要按照上述说明,仔细处理每条指令的最后步骤。
在刷新 CPU 状态时,可以直接使用提供的 flush 函数,以免遗漏。
在判断指令时,可以使用类似于上一步中,判断 Store 指令的方法进行。跳转指令可能数量比较多,记得不要遗漏项目。
如果你额外实现了分支预测,请不要忘了调用前端的更新函数。
在 Store 指令写入内存的时候,你必须使用封装好的 writeMemoryHierarchy 函数,否则你不方便判断 CPU 是否有数据缓存。该函数的描述如下:
/**
* @brief 写入存储结构
*
* @param address 地址,0x80400000 - 0x807fffff
* @param data 数据,按照 4 字节对齐
* @param byteEnable 字节使能
* @return true 写入完成
* @return false 写入未完成
*/
virtual bool Backend::writeMemoryHierarchy(unsigned address,
unsigned data,
unsigned byteEnable);
bool BackendWithCache::writeMemoryHierarchy(unsigned address,
unsigned data,
unsigned byteEnable) override;
实际上,从 Store Buffer 中弹出的项目已经是帮你对齐好的了,因此可以直接使用,示例代码如下:
if (entry.inst == SB || entry.inst == SH || entry.inst == SW) {
StoreBufferSlot stSlot = storeBuffer.front();
bool status =
writeMemoryHierarchy(stSlot.storeAddress, stSlot.storeData, 0xF);
if (!status) {
return false;
} else {
storeBuffer.pop();
}
}
将上述代码添加到你的代码的合适位置处,即可完成对 Store 指令的特殊处理。
仔细地处理完提交部分的内容之后,我们就完成了整个 Tomasulo 实验了。请继续阅读框架的说明文档,完成作业的验证和提交。