跳转至

实验框架使用

可以修改的文件

  1. backend/backend_commit.cpp 编写后端指令提交逻辑,以及指令流入后端的逻辑
  2. frontend/frontend_wp.cpp 编写前端分支预测逻辑
  3. include/reservation_station.hpp 编写保留站唤醒和发射逻辑
  4. include/branch_predict_model.h 设计分支预测相关的数据结构
  5. include/with_predict.h 向前端增加你新设计的数据结构
  6. backend/load_buffer.cppbackend/store_buffer.cpp 完成 Load Buffer 和 Store Buffer 的实现。

其余代码文件不可以修改。

如果进行了修改,你需要保证你的模拟器在不改动其他文件的情况下仍然可以执行。

.gitlab-ci.yml 文件原则上不可修改,否则实验成绩以零分处理(详见评分细则)。

部分接口说明

模拟器中的每个函数都进行了必要的注释,如果有框架理解的问题,请在课程群或者网络学堂答疑区进行提问。下面只会描述实验过程中需要使用或者需要实现的接口。

框架使用了 std::optional<> 等在新标准中出现的内容,详细使用方法可以自行检索。

通用接口

getFUType

在头文件 instruction.h 中。

用于获取指令对应的执行单元。

FUType 为 enum class,在 defines.h 中定义。

特别地,FUType::NONE 表示该指令不需要进入任何执行单元。在本实验中,只有 EXTRA::EXIT 为这种指令。

MaskedLiteral 和 Instruction

include/masked_literal.h 中,定义了本实验中需要使用的所有指令。

Instruction 为抽象的指令模型,用于指令解码和执行。

如果需要判断某个 Instruction 是否为某类指令,可以使用 ==,例子如下:

#include <assert.h>

#include "instructions.h"
#include "masked_literal.h"
...
Instruction inst(0x00000013);
assert(inst == RV32I::ADDI);

也可以使用 != 等符号,instruction还可以重载了 << 符号,可以使用 std::ostream 进行输出。更详细的内容可以参见 include/masked_literal.hinclude/instructions.h

Logger

实验框架提供了一个简易的 Logger 用于向 stderr 中按照同一格式进行输出。

同时可以设置每个层级的信息是否输出。信息一共分为 Debug, Info, Warning, Error 四级。

使用方法与 printf 类似,例子如下:

#include "logger.h"
#include <sstream>
Instruction inst(0x00000013);
std::stringstream ss;
ss << inst;
Logger::setInfoOutput(true);
Logger::Info("Instruction is %s", ss.str().c_str());

输出如下:

[  INFO   ] Instruction is ADDI zero, zero, zero

你可以使用其他的第三方 Logger 来美化输出,但请不要更改已有的输出的格式和内容。

Reorder Buffer 接口

框架提供了 Reorder Buffer 的基础实现。Reorder Buffer 的实现使用了循环队列 buffer 的方法。详细接口说明和实现可以参见 backend/rob.cpp 中的注释。

bool canPush() const; // 检查 ROB 是否有空间插入新项目
bool canPop() const;  // 检查 ROB 是否可以弹出项目
unsigned push(const Instruction &x, bool ready);  // 向 ROB 中插入新项目,可以决定该项是否已经 ready
void pop();  // 从 ROB 中 弹出项目
void flush(); // 清空 ROB
std::optional<ROBEntry> getFront() const; // 获取 ROB popPtr 指向的 ROB 项,若该项不 valid,则返回 std::nullopt
void writeState(const ROBStatusWritePort &x); // 向 ROB 写入指令执行结果,并标记 ready
unsigned getPopPtr() const; // 获取 popPtr 
unsigned read(unsigned addr) const; // 读取对应项的 state.result
bool checkReady(unsigned addr) const; // 检查对应项是否 ready

Store Buffer 接口

框架提供了 Store Buffer 的基础实现。Store Buffer 的实现使用了循环队列 buffer 的方法。详细接口说明和实现可以参见 backend/store_buffer.cpp 中的注释。

为保证 store buffer 不溢出,我们将 store buffer 的大小设置成与 reorder buffer 相同。

void push(unsigned addr, unsigned value); // 向 store buffer 中插入新项目
StoreBufferSlot pop(); // 从 store buffer 中弹出项目
void flush(); // 清空 store buffer
std::optional<unsigned> query(unsigned addr, unsigned robIdx, unsigned robPopPtr); // TODO: 查询 store buffer, 若对应地址命中,返回最新的结果,否则返回 std::nullopt

Load Buffer 接口

框架提供了 Load Buffer 的基础实现。与 Store Buffer 不同,Load Buffer 的表项编号与 ROB 保持一致。详细接口说明和实现可以参见 backend/load_buffer.cpp 中的注释。

void LoadBuffer::push(unsigned addr, unsigned robIdx); // 向 Load Buffer 中推入新项目
LoadBufferSlot LoadBuffer::pop(unsigned robIdx); // 从 Load Buffer 中弹出项目
void LoadBuffer::flush(); // 清空 load buffer
void LoadBuffer::check(unsigned addr, unsigned robIdx, unsigned robPopPtr); // TODO: 根据 store 指令信息,查询并无效化 load buffer 中的对应项目

Processor 处理器抽象

Processor 的步进函数实现如下,可以进行模拟器执行顺序的参考。

bool Processor::step() {
    bool finish = backend.step(frontend);
    auto newInst = frontend.step();
    if (newInst.has_value()) {
        if (!backend.dispatchInstruction(newInst.value()))
            frontend.haltDispatch();
        else {
            std::stringstream ss;
            ss << newInst.value();
            Logger::Info("Dispatching %s with pc = %08x\n", ss.str().c_str(), newInst.value().pc);
        }
    }
    return finish;
}

Register File

框架提供了 Register File 的基础实现。详细接口说明和实现可以参见 common/register_file.cpp 中的注释。

unsigned read(unsigned addr) const;  // 读寄存器
void write(unsigned addr, unsigned val, unsigned robIdx); // 写寄存器,当 robIdx 匹配时清除 busy
bool isBusy(unsigned addr) const;  // 查询寄存器是否 busy
void markBusy(unsigned addr, unsigned robIdx); // 标记寄存器 busy,同时设置占用 ROB 下标
unsigned getBusyIndex(unsigned addr) const; // 获取对应的占用 ROB 下标
void flush(); // 清除 busy 标记

保留站

由于保留站的实现带有模板参数,因此其定义和实现均在文件 include/reservation_station.hpp 中。模板参数 size 为保留站大小。

bool hasEmptySlot() const; // 检查保留站是否已满
void insertInstruction(const Instruction &inst,
                       unsigned robIdx,
                       std::shared_ptr<RegisterFile> regFile,
                       const ReorderBuffer &reorderBuffer); // TODO: 向保留站插入新指令,构造 issue slot
void wakeup(const ROBStatusWritePort &x); // cdb 唤醒指令
bool canIssue() const; // TODO: 检查是否有 issue slot 可以发射
IssueSlot issue(); // TODO: 发射一个 issue slot
void flush(); // 清空保留站

后端接口

Backend 的接口定义如下:

bool dispatchInstruction(const Instruction &inst); // TODO: 前端调用,向后端发射新指令
bool step(Frontend &frontend); // 后端步进函数
unsigned read(unsigned addr) const; // 读取数据内存
bool commitInstruction(const ROBEntry &inst, Frontend &frontend); // TODO:提交一条指令

前端接口

Frontend 的接口定义如下:

protected:
    virtual BranchPredictBundle bpuFrontendUpdate(unsigned int pc); // Optional TODO: 分支预测逻辑
public:
    virtual std::optional<Instruction> step() final;  // 前端步进函数
    virtual void jump(unsigned int jumpAddress) final; // 后端调用接口,发生跳转
    virtual void haltDispatch() final; // 后端调用接口,表示指令不能发射
    virtual unsigned calculateNextPC(unsigned pc) const; // Optional TODO: 分支    预测计算 next pc 逻辑
    virtual void bpuBackendUpdate(const BpuUpdateData &x); // Optional TODO: 分支预测后端更新逻辑

分支预测需要指令带有额外的预测信息进行处理,这些数据结构定义在 include/branch_predict_model 中,可以进行修改,但请不要改动已有的项目,部分计算逻辑基于这些信息,如 mispredict = predictJump != actualJump || jumpTarget != predictTarget;

评测器 checker

代码在 program/checker.cpp 中。

使用方式如下:

Tomasulo Simulator Tester
Usage:
  tomasulo-tester [OPTION...]

  -o, --output arg    Output Log file (default: output.log)
  -h, --help          Print Usage
  -f, --file arg      Input elf file
  -c, --chk-file arg  Check file
  -d, --debug         Print debug infos
  -p, --predict       Use frontend with predictor

一个简单的使用例子:

./checker -f ./test/selection_sort -c ../checkfiles/selection_sort.chk

正确的输出如下:

[  INFO   ] Reading ELF file ./test/selection_sort
[   OK    ] Testcase 1, WORD matched
[   OK    ] Testcase 2, WORD matched
[   OK    ] Testcase 3, WORD matched
[   OK    ] Testcase 4, WORD matched
[   OK    ] Testcase 5, WORD matched
[   OK    ] Testcase 6, WORD matched
[   OK    ] Testcase 7, WORD matched
[   OK    ] Testcase 8, WORD matched
[   OK    ] Testcase 9, WORD matched
[   OK    ] Testcase 10, WORD matched
[   OK    ] Testcase 11, WORD matched
[   OK    ] Testcase 12, WORD matched
[   OK    ] Testcase 13, WORD matched
[   OK    ] Testcase 14, WORD matched
[   OK    ] Testcase 15, WORD matched
[   OK    ] Testcase 16, WORD matched
[   OK    ] 16 testcase(s) passed

上述指令执行了 selection_sort 测例,测例的实现代码在 ./test 文件夹下。

如果需要自定义测例,可以直接在该目录下添加新的 cpp 文件。注意测例中不能使用标准库,并以 EXTRA::EXIT 结尾,可以参考该选择排序测例。同时也可以使用内联汇编自定义指令序列,如下:

int main() {
    asm (
        "li t0, 0x12345\n"
        ".word 0x0000000b\n"
        :
        :
        : "t0"    
    );
}

关于内联汇编的更多使用方法,请自行检索。

测例检查文件的编写在 ./checkfiles 文件夹中。不同的工具链由于编译结果不同,需要使用不同的检查文件。编写可以使用如下两种指令,分别检查内存和寄存器。

RAM <address in hex> <value>
REG <address> value

测试程序的编译结果放在 ./build/test/ 文件夹下,有程序的 elf 和反汇编 asm。调试时可以参考该反汇编文件。


最后更新: 2024年2月23日
作者:cuibst (98.91%), hogura (1.09%)