Skip to content
Table of contents

Spike 运行机制详解

5 min read ··· views #simulator / #spike

指令获取

Spike 通过 MMU(内存管理单元)加载指令,指令获取由 mmu->load_insn(pc) 函数完成,它根据程序计数器的地址从内存中读取一条指令。

mmu->load_insn(pc):根据当前程序计数器(pc)的值,MMU 从内存中读取指令,返回一个 insn_fetch_t 结构,其中包含要执行的指令及其相关元数据。

Spike 提供指令缓存(ICache)来加速指令获取,避免反复从内存读取相同指令,从而提高仿真性能。

在快速路径中,Spike 直接从指令缓存中获取指令:

 auto ic_entry = _mmu->access_icache(pc);
     -> inline icache_entry_t* access_icache(reg_t addr)
       {
         icache_entry_t* entry = &icache[icache_index(addr)];
         if (likely(entry->tag == addr))
           return entry;
         return refill_icache(addr, entry);
       }
 auto fetch = ic_entry->data;
cpp

缓存未命中或处于慢速路径时,Spike 直接通过 mmu->load_insn() 从内存加载指令,读取原始的指令二进制数据。

读取指令

refill_icache 用于填充指令缓存(ICache)。它从内存读取指令,并将指令及其解码结果缓存起来,后续访问即可直接从缓存获取,无需再次访问内存。

refill_icache 的主要功能是:

  • 地址转换(TLB)。
  • 读取不同长度的指令。
  • 解码指令。
  • 将指令存入缓存。
  • 监控触发器或调试器的状态。

指令解码

processor_t::decode_insn(insn_t insn) 负责把二进制指令解码为对应的操作函数。它先通过哈希表快速查找指令对应的处理函数(insn_func_t);若未命中,再通过线性搜索找到指令描述符,并缓存起来供后续查找。

decode_insn 的主要功能是:

  • 从二进制指令中提取操作码。
  • 查找指令对应的操作函数。
  • 通过哈希表缓存机制加速指令解码。
  • 处理自定义指令集和扩展。

通过哈希表查找指令

 size_t idx = insn.bits() % OPCODE_CACHE_SIZE;
 auto [hit, desc] = opcode_cache[idx].lookup(insn.bits());
cpp
  • insn.bits():提取指令的二进制位表示。
  • idx = insn.bits() % OPCODE_CACHE_SIZE:对 OPCODE_CACHE_SIZE 取模,得到哈希表索引 idx,指向缓存中的某个槽位。
  • opcode_cache[idx].lookup(insn.bits()):在哈希表(opcode_cache)中查找该指令。若命中(hittrue),直接返回指令描述符 desc

处理自定义指令和标准指令

哈希表未命中(hitfalse)时,Spike 改用线性搜索,在自定义指令和标准指令列表中寻找匹配的指令。

 if (unlikely(!hit)) {
   // fall back to linear search
   auto matching = [insn_bits = insn.bits()](const insn_desc_t &d) {
     return (insn_bits & d.mask) == d.match;
   };
   auto p = std::find_if(custom_instructions.begin(),
                         custom_instructions.end(), matching);
   if (p == custom_instructions.end()) {
     p = std::find_if(instructions.begin(), instructions.end(), matching);
     assert(p != instructions.end());
   }
   desc = &*p;
   opcode_cache[idx].replace(insn.bits(), desc);
 }
cpp
  • 线性搜索:缓存未命中时,Spike 在 custom_instructions(自定义指令集)中线性搜索。matching 是一个匿名函数,通过指令的掩码(mask)和匹配值(match)来匹配指令。
  • std::find_if:在列表中查找第一个满足条件的指令描述符。若自定义指令集中未找到,Spike 继续搜索标准的 instructions 集合。
  • 缓存更新:找到匹配的指令编码后,Spike 将描述符插入哈希表缓存,加速后续解码。

指令什么时候被添加到队列中?

processor_t::processor_t 构造函数中,processor_t::register_base_instructions() 负责将指令添加到 instructions 容器,这也是标准 RISC-V 指令集注册的关键函数。

   #define DEFINE_INSN(name) \
     if (!name##_overlapping) \
       register_base_insn((insn_desc_t) { \
         name##_match, \
         name##_mask, \
         fast_rv32i_##name, \
         fast_rv64i_##name, \
         fast_rv32e_##name, \
         fast_rv64e_##name, \
         logged_rv32i_##name, \
         logged_rv64i_##name, \
         logged_rv32e_##name, \
         logged_rv64e_##name});
   #include "insn_list.h"
   #undef DEFINE_INSN
cpp
  • DEFINE_INSN:为每条指令声明相关的处理函数。每条指令有多个变种(如 RV32I、RV64I、RV32E、RV64E),每个变种都有对应的快速路径(fast)和日志记录(logged)版本。
 DEFINE_INSN(add)
 DEFINE_INSN(addi)
 DEFINE_INSN(addiw)
 DEFINE_INSN(addw)
 DEFINE_INSN(and)
 DEFINE_INSN(andi)
 DEFINE_INSN(auipc)
 DEFINE_INSN(beq)
 DEFINE_INSN(bge)
 DEFINE_INSN(bgeu)
 DEFINE_INSN(blt)
cpp
  • insn_list.h:包含所有标准 RISC-V 指令的列表。通过包含该文件,Spike 为每条指令声明了相关的处理函数。
 static uint32_t addi(unsigned int dest, unsigned int src, uint16_t imm) __attribute__ ((unused));
 static uint32_t addi(unsigned int dest, unsigned int src, uint16_t imm)
 {
   return (bits(imm, 11, 0) << 20) |
     (src << 15) |
     (dest << 7) |
     MATCH_ADDI;
 }
cpp

随后调用 processor_t::register_insn 函数,将指令加入指令队列。

void processor_t::register_insn(insn_desc_t desc, bool is_custom) {
  assert(desc.fast_rv32i && desc.fast_rv64i && desc.fast_rv32e && desc.fast_rv64e &&
         desc.logged_rv32i && desc.logged_rv64i && desc.logged_rv32e && desc.logged_rv64e);

  if (is_custom)
    custom_instructions.push_back(desc);
  else
    instructions.push_back(desc);
}
cpp

获得指令执行函数

desc->func(xlen, rve, log_commits_enabled);
cpp

desc->func:指令描述符中包含对应的指令处理函数(insn_func_t 类型),Spike 据此返回该指令的处理函数。

insn_func_t func(int xlen, bool rve, bool logged) const
  {
    if (logged)
      if (rve)
        return xlen == 64 ? logged_rv64e : logged_rv32e;
      else
        return xlen == 64 ? logged_rv64i : logged_rv32i;
    else
      if (rve)
        return xlen == 64 ? fast_rv64e : fast_rv32e;
      else
        return xlen == 64 ? fast_rv64i : fast_rv32i;
  }
cpp

此时得到的指令执行函数形如 fast_rv32i_add,但实际并不存在这个函数,指令执行逻辑是通过宏定义实现的。

指令执行

获得指令的执行函数后,Spike 直接调用它。

static inline reg_t execute_insn_fast(processor_t* p, reg_t pc, insn_fetch_t fetch) {
  return fetch.func(p, fetch.insn, pc);
}
cpp

Spike 的指令逻辑定义在 ISA(指令集架构)模块中,具体位于 riscv/insns/ 目录下。

  • 每条指令对应一个 C++ 文件,例如 add 指令的编码格式定义在 riscv/insns/add.h 中。Spike 使用宏和位运算来解析和生成指令的二进制编码。
  • 指令的解析和执行由仿真器内部的解码器完成。指令的二进制编码由 RISC-V ISA 标准规定,在 Spike 中由解码器解析并映射到相应的指令处理函数。

Spike 通过宏定义实现指令执行逻辑,让多条指令共享相似的结构,简化重复代码的编写。例如,add.h 中的宏实现可被 RV32I、RV64I 等多个变种复用,无需为每个变种单独编写函数。

构建时,Spike 的编译器会将宏展开为实际的指令处理逻辑。具体执行流程如下:

  1. 仿真器解码到 add 指令时,通过指令表中的描述符(如 fast_rv32i_add)找到 add.h 中的处理逻辑。
  2. add.h 中的宏展开为相应的操作,仿真器据此执行加法指令。
// add.h
WRITE_RD(RS1 & RS2);
|
||-> // riscv/decode_macros.h
    #define RS1 READ_REG(insn.rs1())
    #define RS2 READ_REG(insn.rs2())
||-> // riscv/decode_macros.h
    #define WRITE_RD(value) WRITE_REG(insn.rd(), value)
    #define WRITE_REG(reg, value) ({ \
        reg_t wdata = (value); /* value may have side effects */ \
        if (DECODE_MACRO_USAGE_LOGGED) STATE.log_reg_write[(reg) << 4] = {wdata, 0}; \
        CHECK_REG(reg); \
        STATE.XPR.write(reg, wdata); \
      })
cpp

以上是 Spike 运行时指令获取与指令执行的大体流程。