RV32I 基本整数指令集分析

发布时间 2023-11-15 05:01:31作者: 吴建明wujianming

RV32I 基本整数指令集分析

RV32I 被设计成足以构建一个编译器目标机,并支持现代操作系统环境。 这个 ISA 也被设计成在最小实现时减少所需的硬件。RV32I 包括了 47 条单独的 指令,虽然某个简单的实现可以使用一条 SYSTEM 硬件指令将 8 条ECALL/EBREAK/CSRR*,指令全部用自陷实现,并将 FENCE 指令和 FENCE.I 指令都作为 NOP 指令实现,这将把硬件指 令数减少到总共 38 条。RV32I 可以仿真几乎所有其他的 ISA 扩展(除了 A 扩展, 它需要额外的硬件以支持原子性)。

2.1 基本整数子集的程序员模型 图 2.1给出了基本整数子集的用户可见状态。有31个通用寄存器x1~x31,它们保存了整 数数值。寄存器x0是硬件连线的常数0。没有硬件连线的子程序返回地址连接寄存器,但是 在一个过程调用中,标准软件调用约定使用寄存器x1来保存返回地址。对于RV32,其x寄存 器是32位宽度的,对于RV64,它们是64位宽度的。本文档使用术语XLEN来指明当前x寄存器 的宽度(不是32就是64)。 还有一个额外的用户可见寄存器:程序计数器pc保存了当前指令的地址。

 

可用的体系结构寄存器数量,对代码大小、性能和能耗有巨大的影响。虽 然有争论说 16 个寄存器对于一个运行编译代码的整数 ISA 来说足够了,但是 在使用 3 地址格式的 16 位指令中编码 16 个寄存器,从而实现一个完整的 ISA 几乎是不可能的。16 个寄存器,需要 4 位来区别。3 地址格式,就需要使用 12 位来编码,对于 16 位指令来说,留给操作码的只有 4 位了,几乎不可能。虽然一个 2 地址格式是可行的,它将增加指令数目并降低效率。  想避免出现中间的指令长度(例如 Xtensa 的 24 位指令)来简化基本硬件 实现,并且一旦采用 32 位指令长度,支持 32 个寄存器就是直截了当的事情。 更大的整数寄存器数量也有助于提高高性能代码的性能,可以大量使用循环展 开(loop uunrolling)、软件流水线(software pipelining)和 cache 分块(cache tiling)。

基于这些原因, 在基本 ISA 上选择了传统大小 32 个整数寄存器。动态寄存器使用趋向于被几个频繁访问的寄存器所左右,并且寄存器文件实现可 以针对频繁访问的寄存器进行优化,减少访问能耗[26]。可选的压缩 16 位指 令格式大部分时间仅仅访问 8 个寄存器,并且因此可以提供密集的指令编码, 同时额外的指令集扩展如果需要的话,可以支持大得多的寄存器空间(平坦的 或者层次的)。 对于资源约束的嵌入式应用, 定义了 RV32E 子集,它只有 16 个寄存器。

2.2 基本指令格式 在基本ISA中,有四种核心指令格式(R/I/S/U),如图 2.2所示。所有的指令都是固定 32位长度的,并且在存储器中必须在4字节边界对齐。当发生一个条件分支或者无条件转移 而且目标地址不是对齐到4字节时,将会产生一个指令地址不对齐的异常。如果条件分支没 有发生(not taken),那么将不会产生一个取指不对齐异常。

 

在所有格式中,RISC-V ISA将源寄存器(rs1和rs2)和目标寄存器(rd)固定在同样的位 置,以简化指令译码。在指令中,立即数被打包,朝着最左边可用位的方向,并且是分配好 的,以减少硬件复杂度。特别地,所有立即数的符号位总是在指令的第31位,以加速符号扩 展电路。 解码寄存器区分符通常处于实现的关键路径上,因此指令格式选择将所有 寄存器区分符,在所有格式中,都固定在相同的位置。这是有代价的,不得不 把立即数的一些位分散到格式中(这是与 RISC-IV,也就是 SPUR[14],相同的 特性)。 事实上,绝大多数立即数要么很小,要么需要所有的 XLEN 位。 选择了一种非对称的立即数切分方法(在常规的指令中的低 12 位,加上一条特殊 的 load 立即数指令提供高 20 位)来增加常规指令的可用操作码空间。(译者 注:为了加载一个 32 位立即数,需要两步:load 指令提供该立即数的高 20 位[31:12],常规指令提供该立即数的低 12 位[11:0],最后拼接成一个 32 位立 即数)。另外,这些立即数都是符号扩展的有符号数。 并没有 观察到使用零扩展无符号数带来的好处,并且 想把 ISA 做得 尽可能简单。 2.3 立即数编码变种 基于立即数处理,还有额外两种指令格式变种(SB/UJ),如图 2.3所示。 在图 2.3中,每个立即数字段被所生成的立即数值中的位的位置(imm[x])标签,而不 是在指令的立即数字段中的通常位的位置。图 2.4给出了每一种基本指令格式生成的立即数,并被标签,以显示哪个指令位(inst[y])生成了立即数值中的哪个位。

 

图 2.3:RISC-V 显示了立即数的基本指令格式

 

图 2.4:RISC-V 指令生成的立即数。用指令的位标注了用于构成立即数的字段。符号扩展总 是使用 inst[31]。

S和SB格式唯一的区别在于,在SB格式中,12位立即数字段用于编码2的倍数的分支偏移 量。与通常在硬件中将编码在指令中的立即数所有位向左移动1位不同,此处中间位 (imm[10:1])和符号位保持在固定的位置,而S格式中的最低位(inst[7])编码为SB格式中 的高位(imm[11])。 类似的,U和UJ格式唯一的区别在于,20位立即数被左移12位以生成U立即数,而被左 移1位以生成J立即数。在U和UJ格式立即数,其在指令中的位置的选择,以最大化与其它指 令的相互覆盖,以及最大化U和UJ格式立即数的相互覆盖。

立即数的符号扩展是最关键的操作之一(特别是在 RV64I 当中),而在 RISC-V 中,所有立即数的符号位总是在指令的 31 位,这允许符号扩展操作可 以和指令译码并行。 虽然更为复杂的实现可能对分支和跳转计算有独立的加法器,因此也不会 受益于将所有指令中的立即数位置保持固定不变,但是 想降低简单实现的 硬件代价。通过在指令中旋转位,来对 B 和 J 立即数进行编码,而不是使用动 态硬件多路选择器(mux)来将立即数乘 2, 减少了大约 2 倍的指令信号 的扇出(fanout)和立即数多路选择器。混乱的立即数编码将给静态或者预先 编译带来可忽略的时间开销。对于动态生成指令,则有一些小的额外开销,但 是最常见的向前短分支具有直截了当的立即数编码。

2.4 整数计算指令

绝大多数整数计算指令对保存在整数寄存器中的XLEN位值进行操作。整数计算指令要 么使用I类格式编码为寄存器-立即数操作,要么使用R类格式编码为寄存器-寄存器操作。对 于寄存器-立即数指令和寄存器-寄存器指令,其目标都是寄存器rd。没有整数计算指令产生算术异常。

 并没有包含特殊的指令集支持整数算术操作的溢出检测,因为许多溢 出检测可以使用 RISC-V 分支指令以较低的代价实现。无符号数加法的溢出检 测,只需要在加法后执行一条额外的分支指令。类似的,有符号数组边界检测, 也只需要一条分支指令。有符号数加法溢出检测,需要几条指令,这与加数是 一个立即数还是一个变量有关。 考虑过添加分支指令,用于检测它们的有 符号数寄存器操作数的和是否会溢出,但是最终选择了将这些指令从基本 ISA 中去掉。

整数寄存器-立即数指令

 

ADDI将符号扩展的12位立即数加到寄存器rs1上。算术溢出被忽略,而结果就是运算结 果的低XLEN位。ADDI rd,rs1,0用于实现MV rd,rs1汇编语言伪指令。 SLTI(set less than immediate)将数值1放到寄存器rd中,如果寄存器rs1小于符号扩展的 立即数(比较时,两者都作为有符号数),否则将0写入rd。SLTIU与之相似,但是将两者作 为无符号数进行比较(也就是说,立即数被首先符号扩展为XLEN位,然后被作为一个无符 号数)。注意,SLTIU rd,rs1,1将设置rd为1,如果rs1等于0,否则将rd设置为0(汇编语言伪指 令SEQZ rd,rs)。ANDI、ORI、XORI是逻辑操作,在寄存器rs1和符号扩展的12位立即数上执行按位AND、OR、XOR操作,并把结果写入rd。注意,XORI rd,rs1,-1在rs1上执行一个按位取反操作(汇编 语言伪指令NOT rd,rs)。

 

被移位常数次,被编码为I类格式的特例。被移位的操作数放在rs1中,移位的次数被编 码到I立即数字段的低5位。右移类型被编码到I立即数的一位高位。SLLI是逻辑左移(0被移 入低位);SRLI是逻辑右移(0被移入高位);SRAI是算术右移(原来的符号位被复制到空出的高位中)。

 

LUI(load upper immediate)用于构建32位常数,并使用U类格式。LUI将U立即数放到目标寄存器rd的高20位,将rd的低12位填0。 AUIPC(add upper immediate to pc)用于构建pc相对地址,并使用U类格式。AUIPC从20 位U立即数构建一个32位偏移量,将其低12位填0,然后将这个偏移量加到pc上,最后将结 果写入寄存器rd。 对于控制流转移和数据访问,AUIPC 指令支持双指令序列,以从当前 pc 访问任意偏移地址。通过一条 AUIPC 指令和一条 12 位立即数 JALR 指令的组合, 可以将控制转移到任意 32 位 pc 相对地址;而一条 AUIPC 指令加上一条 12 位 立即数偏移的常规 load 或者 store 指令,可以访问任意 32 位 pc 相对数据的地 址。 当前 pc 的值,可以通过将 U 立即数设置为 0 来读取。虽然一条 JAL+4 指 令也可以获得 pc 值,但是它在简单的实现中可能会导致流水线停顿,或者在 更复杂的微体系结构中,导致 BTB 结构被污染。JAL+4 实际上要执 行一个分支操作。这个分支会对流水线造成负面影响。而 AUIPC 则不会,它只是运算指令。

整数寄存器-寄存器操作。

RV32I定义了几种算术R类操作。所有操作都是读取rs1和rs2寄存器作为源操作数,并把 结果写入到寄存器rd中。funct7和funct3字段选择了操作的类型。

 

ADD和SUB分别执行加法和减法。溢出被忽略,并且结果的低XLEN位被写入目标寄存器 rd。SLT和SLTU分别执行符号数和无符号数的比较,如果rs1<rs2,则将1写入rd,否则写入0。注意,SLTU rd,x0,rs2,如果rs2不等于0。在RISC-V中,x0寄存器永远是0,则把1写 入rd,否则将0写入rd(汇编语言伪指令SNEZ rd,rs)。AND、OR、XOR执行按位逻辑操作。 SLL、SRL、SRA分别执行逻辑左移、逻辑右移、算术右移,被移位的操作数是寄存器rs1,移 位次数是寄存器rs2的低5位。

NOP 指令

 

NOP指令并不改变任何用户可见的状态,除了使得pc向前推进。NOP被编码为ADDI x0,x0,0。

NOP 可用于将代码段对齐到对微体系结构有重要作用的地址边界上,或 者给内联(inline)代码修改保留空间。虽然有很多种编码可以成为 NOP,定义了一个正规的 NOP 编码,允许微体系结构对此进行优化,同时也使得 反汇编输出更具可读性。 2.5 控制转移指令 RV32I提供了两类控制转移指令:无条件跳转和条件分支。RV32I中的控制转移指令,并 没有体系结构可见的分支延迟槽。 无条件跳转 跳转并连接(JAL)指令使用了UJ类格式,此处J立即数编码了一个2的倍数的有符号偏移量。这个偏移量被符号扩展,加到pc上,形成跳转目标地址,跳转范围因此达到±1MB。 JAL将跳转指令后面指令的地址(pc+4)保存到寄存器rd中。标准软件调用约定使用x1来作为返回地址寄存器。普通的无条件跳转指令(汇编语言伪指令J)被编码为rd=x0的JAL指令。x0是 只读寄存器,无法写入。

 

间接跳转指令JALR(jump and link register)使用I类编码。通过将12位有符号I类立即数 加上rs1,然后将结果的最低位设置为0,作为目标地址。跳转指令后面指令的地址(pc+4) 保存到寄存器rd中。如果不需要结果,则可以把x0作为目标寄存器。

 

JAL指令和JALR指令会产生一个非对齐指令取指异常,如果目标地址没有对齐到4字节边 界。 所有的无条件跳转指令都是用 pc 相对寻址,这有助于支持位置无关代码。 JALR 指令被定义为可以使用双指令序列来跳转到 32 位绝对地址空间的任何地 方。首先一条 LUI 指令将目标地址的高 20 位加载到 rs1 中,然后 JALR 指令可 以加上低 12 位。类似的,AUIPC 指令,然后 JALR 指令就可以跳转到 32 位绝 对地址空间的任何地方。

注意到 JALR 指令并没有把 12 位立即数作为 2 字节的倍数看待,这与条件 分支指令不同。这避免了在硬件上多出一种立即数格式。事实上,绝大多数 JALR 指令的使用要么是一个立即数 0,要么与 LUI 或者 AUIPC 成对使用,因此在范 围上的稍微减小,影响并不显著。 JALR 指令忽略了计算出来的目标地址的最低位。这不但稍微简化了硬件,同时也允许函数指针的最低位可以用于存放额外的信息。虽然此种情形下,可能会有潜在轻微的误差损失,实际上跳转到一个不正确的指令地址,通常将很 快会引起一个异常。

在支持 16 位对齐指令扩展的机器上,例如压缩指令集扩展 C,不可能产 生指令取指非对齐异常。 返回地址预测栈,是高性能指令取指单元的一种常见特性。 注意到 rd 和 rs1 可用于指导一个实现的指令取指预测逻辑,指示 JALR 指令是否应当 push(rd=x1)、pop(rd=x0,rs1=x1)还是不操作(其余情况)一个返回地址栈。

类似的,一条 JAL 指令只有在 rd=x1 的时候,才能将返回地址 push 到返回地址 栈中。 当 rs1=x0 时,JALR 可完成一个单一指令的过程调用,实现从任意地址空 间对最低的 2KB 或者最高的 2KB 地址区域进行调用,这可用于实现对小的运 行时库的快速调用。 条件分支 所有分支指令使用SB类指令格式。12位B立即数编码了以2字节倍数的有符号偏移量, 并被加到当前pc上,生成目标地址。条件分支范围是±4KB。

 

分支指令比较两个寄存器。BEQ和BNE将跳转,如果rs1和rs2相等或者不相等。BLT和BLTU 将跳转,如果rs1小于rs2,分别使用有符号数和无符号数进行比较。BGE和BGEU将跳转,如 果rs1大于等于rs2,分别使用有符号数和无符号数进行比较。注意,BGT、BGTU、BLE和BLEU 可以通过将BLT、BLTU、BGE、BGEU的操作数对调来实现。 软件应当优化,使得顺序代码路径是最常见执行路径,而频率较少的跳转执行代码则放 到直线路径之外。软件同时也应当假设向回(向后)跳转总是被预测跳转的,而向前(向下)跳转总是被预测不跳转的,至少第一次碰到分支指令的时候,是这样的。动态分支预测器将 很快学会任何可以预测的分支行为。 与其它某些体系结构不同,无条件跳转应当总是使用RISC-V的跳转(rd=x0的JAL)指令, 而不是一条条件永远为真的条件分支指令。RISC-V跳转总是pc相对寻址的,并且比分支指令 支持大得多的偏移量范围,而且还不会对条件分支预测表造成压力。现代处理器 都有条件分支预测器,对每一条碰到的条件分支指令,都会记录其结果,以便后面对其进行预测。

条件分支指令被设计为在两个寄存器之间进行算术比较操作(如同 PA-RISC 和 Xtensa ISA 做的那样),而不是使用条件码(x86、ARM、SPARC、 PowerPC),也不是只将寄存器与零进行比较(Alpha、MIPS),也不是比较两个 寄存器相等(MIPS)。这样的设计灵感来自 观察到,一条组合了比较和分 支的指令,可以很好地适应常规的流水线,避免了使用额外的条件码状态或者 使用临时寄存器,减少了静态代码大小、降低了动态指令取指通信量。另外一 点是,将寄存器与零比较需要不可忽视的电路延迟(non-trivial circuit delay) (特别是在先进工艺中使用静态逻辑的时候),因此几乎和算术比较是一样昂 贵的操作。将比较和分支融合成一条指令的另外一个好处是,分支指令会在指 令流的前端被检测到。分支预测功能,需要在取指段即判断是否是 条件分支指令,而不会像通常指令一样等到译码段,以便可以提前进行预测。 采用条件码的设计,也许在当基于相同条件码而执行多个分支时,有一些好处, 但是 相信这种情形相对来说比较少见。  考虑过但是并没有在指令编码中加入静态分支提示(static branch hints)。某些处理器允许在指令中加入提示位,告知此条分支指令是 否会执行分支。静态分支提示可以降低对动态分支预测器的压力,但是需要 更多的指令编码空间以及软件 profiling 才能取得较好的效果,而且一旦程序的 运行与 profiling 运行不同时,性能会大幅度下降。  考虑过但并没有加入条件传输或者条件执行指令。原文为 predicted instruction,为方便理解,翻译为条件执行指令。类似 ARM 中,每条指令都是条件执行的,在每条指令的头部,都有 4 位条件码,只有符合条 件码,这条指令才能被正常执行,否则就是空操作),它们可以有效地替代不 可预测的向前短分支指令。条件传输指令在两者中较为简单,但是当条件码可 能产生异常时(存储器访问和浮点运算),变得难以使用。条件执行指令在系 统中加入了额外的标志状态,需要额外的指令来设置和清除这些标志,需要对 每条指令有额外的编码开销。条件传输指令和条件执行指令对于乱序执行微体 系结构来说,都增加了复杂性,当出现预测错误时,增加了隐式的第三个源操 作数,因为需要把目标体系结构寄存器的原始值,复制到重命名物理寄存器中。在乱序执行结构中,需要解决寄存器相关,因此引入了重命名物理寄存器,程序员可见的称为体系结构寄存器。当出现分支预测错误时,需要把寄存器恢复到分支预测前的状态。另外,使用静态编译时的决定来预测, 而不是使用分支来预测,在使用没有包含在编译时的输入训练集的时候,将会 得到较低的性能,特别是实际上不可预测的分支非常稀少,而且随着分支预测 技术的进步,不可预测的分支变得更为稀少。

 注意到,现存各种微体系结构技术可以将不可预测的向前短分支指令, 在内部动态地转换为条件执行的代码,以避免当出现预测错误时清空流水线 [9][13][12],这在一些商用处理器中得到了实现[22]。最简单的技术就是通过仅 仅清空受到分支指令影响的其他指令,而不是清空整个流水线,或者通过使用 更宽的指令取指同时从分支的两边取指,或者暂停取指,这样可以减少预测错 误时带来的恢复开销。乱序执行内核采用更为复杂的技术,在受到分支影响的 其他指令上,附加上内部的预测信息,这些预测信息是分支指令写入的,这样 就允许分支指令以及其后的指令可以推测地执行,并可以和其他代码一样乱序 执行[22]。 2.6 Load 和 store

RV32I是一个load-store体系结构,也就是说,只有load和store指令可以访问存储器,而 算术指令只在CPU寄存器上进行操作运算。RV32I提供了一个32位用户地址空间,它是字节 寻址并且是小端的。执行环境将定义这个地址空间的哪些部分是可以合法访问的,这涉及到存储保护等。

 

Load和store指令在寄存器和存储器之间传输数值。Load指令编码为I类格式,而store指 令编码为S类格式。有效字节地址是通过将寄存器rs1与符号扩展的12位偏移量相加而获得的。 Load指令将存储器中的一个值复制到寄存器rd中。Store指令将寄存器rs2中的值复制到存储 器中。 LW指令将一个32位数值从存储器复制到rd中。LH指令从存储器中读取一个16位数值, 然后将其进行符号扩展到32位,再保存到rd中。LHU指令存储器中读取一个16位数值,然后 将其进行零扩展到32位,再保存到rd中。对于8位数值,LB和LBU指令的定义与前面类似。SW、 SH、SB指令分别将从rs2低位开始的32位、16位、8位数值保存到存储器中。 为了获得最高的性能,所有load和store指令的有效地址,应该与该指令对应的数据类型 相对齐(也就是说,32位访问应该在4字节边界对齐,16位访问应该在2字节边界对齐)。基 本ISA支持非对齐的访问,但是根据实现的不同,这可能会运行得非常慢。更进一步的,对 齐的load和store访问执行时,可以确保是原子性的,而非对齐的load和store可能不能原子性 的完成,因此需要额外的同步来确保原子性。具体实现非对齐访问时,可能一次 访问会被分解为两次存储器访问,这就不是不可分割的原子性操作,有潜在的危险。 非对齐访问在移植遗留代码时有时是需要的,而且在那些使用任何形式 packed-SIMD 扩展的应用程序上,这是取得高性能的基本要求。 通过常规 的 load 和 store 指令支持非对齐访问的根据在于,这样做,可以简化需要额外 非对齐硬件的支持。一种选项是在基本 ISA 中禁止非对齐访问,然后通过某些 单独的 ISA 来提供对非对齐访问的支持,要么是通过某些特殊指令帮助软件处 理非对齐访问,要么是一个新的非对齐硬件寻址模式。特殊指令是难以使用的, 导致 ISA 复杂化,并且通常会加入新的处理器状态(例如 SPARC VIS 对齐地址 偏移寄存器)或者访问当前处理器状态复杂化(例如 MIPS LWL/LWR 部分寄存 器写)。另外,对于面向循环的 packed-SIMD 代码,当操作数不对齐时导致的 性能开销,促使软件根据操作数的对齐方式提供多种形式的循环样式,这将导 致软件代码产生复杂化,并且导致循环启动时的开销。新的非对齐硬件寻址模 式,在指令编码中占据大量空间或者需要非常简化的寻址模式(例如只有寄存 器间接寻址)。  并没有强制非对齐访问的原子性,因此简化了实现,可以通过使用一 个机器自陷和软件处理函数来处理非对齐访问。如果硬件支持非对齐访问,软 件则可以通过使用常规 load 和 store 指令来简化。硬件则可以依据运行时地址 是否是对齐的,来自动优化访问。 2.7 存储器模型 基本RISC-V ISA在一个单一的用户地址空间内支持多个同时线程的执行。每个RISC-V线程 拥有它自己的寄存器和程序计数器,并执行一段不相关的顺序指令流。执行环境将定义 RISC-V线程是如何创建和管理的。RISC-V线程可以通过调用执行环境或者直接通过共享存储 器系统来在相互之间进行通信和同步,执行环境将在规范的另外文档中描述。RISC-V线程也 可以与I/O设备交互,并可通过对指派给I/O的地址空间部分进行load和store,间接地在I/O设 备间通信。 在基本RISC-V ISA中,每个RISC-V线程看到它自己的存储器操作,如同它们就是按照程序 中的顺序执行一样。RISC-V在线程间有一个放松的存储器模型(relaxed memory model),在不同的RISC-V线程之间的存储器操作,需要一条明确的FENCE指令来确保任何特定地顺序。介绍了可选的原子性存储器指令扩展“A”,它可以在共享存储器空间中提供额外的同步操作。

 

FENCE指令用于顺序化其他RISC-V线程、外部设备或者协处理器看到的设备I/O和存储器 访问。任何设备输入(I)、设备输出(O)、存储器读(R)、存储器写(W)的组合,相对于 其他一样的组合,可能需要按序的。通俗的说,在所有前续集合(predecessor set)执行到 FENCE指令前的任何操作之前,处在FENCE指令后的后续集合(successor set)中的任何操作, 都不能被任何其他RISC-V线程或者外部设备看到。FENCE就像一个栅栏,FENCE之前 所有的存储器操作、I/O操作必须完成后,在FENCE之后的指令才能看到结果。执行环境将 定义什么I/O操作是可能的,特别地,哪些load或者store指令被处理并且作为设备输入或设 备输出操作顺序化,而不是被作为存储器读和写来处理。例如,内存映射I/O(memory-mapped I/O)设备通常被非缓存(uncached)的load和store指令来访问,并使用(FENCE指令中的) I和O位,而不是R和W位。 FENCE指令中未使用的字段imm[11:8]、rs1和rd被保留给未来扩展中更细粒度的栅栏。为 了保持前向兼容性,基本实现应当忽略这些字段,而标准软件应当对这些字段写0。  选择一个放松的存储器模型,以允许对一个简单的机器实现可以得到 较高的性能。放松的存储器模型也非常可能兼容未来协处理器或者加速器扩展。  将 I/O 顺序化与存储器 R/W 顺序化区分开来,以避免在设备驱动程序线 程中不必要的串行化,同时也支持控制附加的协处理器或者 I/O 设备的可选的 非存储器通路。简单的实现可以进一步忽略前续(predecessor)和后续 (successor)字段,而总是对所有操作执行一个保守的栅栏动作。

 

FENCE.I指令用于同步指令和数据流。RISC-V并不能确保在同一个RISC-V线程中,取指看 得到前面对指令存储器的store,直到执行一条FENCE.I指令。一条FENCE.I指令只是保证在一 个RISC-V线程中,该指令之后的取指操作,可以看得到这条指令之前的任何数据store。在多 处理器系统中,FENCE.I指令并不能确保其他RISC-V线程的取指看得到本地线程的store。为了 使得一条对指令存储器的store对所有RISC-V线程可见,写数据的线程必须在要求所有远程 RISC-V线程执行FENCE.I指令之前,执行一条数据FENCE指令。

FENCE.I指令中未使用的字段imm[11:0]、rs1和rd被保留给未来扩展中更细粒度的栅栏。 为了保持前向兼容性,基本实现应当忽略这些字段,而标准软件应当对这些字段写0。 FENCE.I 指令被设计为支持各种实现方式。一种简单的实现就是当执行一 条 FENCE.I 指令时,清空本地指令 Cache 和指令流水线。更复杂一些的实现, 可以在指令(数据)Cache 上监听每一次数据(指令)Cache 失效,或者使用 一个包含的统一私有 L2 Cache(inclusive unified private L2 Cache),当 L2 Cache 的 Cache line 被一条本地 store 指令写时,作废掉主指令 Cache,就是 L1 指令 Cache中对应的 Cache line,这是为了实现运行时修改指令 的目的。运行时修改指令时,首先使用 store 指令写存储器,然后数据会被写 入 L1 数据 Cache,然后写入 L2 统一 Cache。在此,检测到是写入到指令段, 所以 L2 统一 Cache 将反向作废掉 L1 指令 Cache 中的对应行,以保持一致性。 L1 指令 Cache 从程序员角度,是不能写的。如果指令和数据可以以这种方式 保持一致性,那么在执行 FENCE.I 指令时,仅仅需要清空流水线。

 考虑过但没有加入一条“保存指令字”指令(如同 MAJC 中一样[25])。 JIT 编译器可能在一条 FENCE.I 指令之前,生成一个较大的指令 trace,并通过 写翻译后的指令到那些已知不会缓存到指令 Cache 的存储器区域中,来分散任 何指令 cache 监听/作废的开销。 2.8 控制和状态寄存器指令 系统指令用于访问那些可能需要特权访问的系统功能,以I类指令格式编码。这可以分 为两类:一类是原子性读-修改-写控制和状态寄存器(CSR)的指令,另一类是其他特权指 令。CSR指令在本节描述,另外两条其他的用户级SYSTEM指令将在后面一节描述。 系统指令被定义为,允许在简单的实现中,总是自陷到一个单一的软件自 陷处理函数(software trap handler)。更高级的实现,可以在硬件上执行一条或者多条系统指令。

CSR 指令

 在此定义所有的CSR指令,虽然在用户级基本ISA中,只能访问少数几个只读计数器。

 

CSRRW(Atomic Read/Write CSR)指令原子性的交换 CSR 和整数寄存器中的值。CSRRW 指令读取在 CSR 中的旧值,将其零扩展到 XLEN 位,然后写入整数寄存器 rd 中。rs1 寄存器 中的值将被写入 CSR 中。如果 rd=x0,那么这条指令将不会读该 CSR,且不会导致任何因为 CSR 读而出现的副作用。 CSRRS(Atomic Read and Set Bits in CSR)指令读取 CSR 的值,将其零扩展到 XLEN 位,然 后写入整数寄存器 rd 中。整数寄存器 rs1 中的初始值被当做按位掩码指明了哪些 CSR 中的 位被置为 1。rs1 中的任何为 1 的位,将导致 CSR 中对应位被置为 1,如果 CSR 中该位是可 以写的话。CSR 中的其他位不受影响(虽然当 CSR 被写入时可能有些副作用)。 CSRRC(Atomic Read and Clear Bits in CSR)指令读取 CSR 的值,将其零扩展到 XLEN 位, 然后写入整数寄存器 rd 中。整数寄存器 rs1 中的初始值被当做按位掩码指明了哪些 CSR 中 的位被置为 0。rs1 中的任何为 1 的位,将导致 CSR 中对应位被置为 0,如果 CSR 中该位是 可以写的话。CSR 中的其他位不受影响。 对于 CSRRS 指令和 CSRRC 指令,如果 rs1=x0,那么指令将根本不会去写 CSR,因此应 该不会产生任何由于写 CSR 产生的副作用。某些特殊 CSR 检测是否有人尝试写入, 一旦有写入,则执行某些动作。这和写入什么值没什么关系,例如试图访问一个只读 CSR 时产生一个非法指令异常。注意如果 rs1 寄存器包含的值是 0,而不是 rs1=x0,那么将会把 一个不修改的值写回 CSR。这时会有一个写 CSR 的操作,但是写入的值就是旧值, 因此可能会产生副作用。 CSRRWI 指令、CSRRSI 指令、CSRRCI 指令分别于 CSRRW 指令、CSRRS 指令、CSRRC 指令 相似,除了它们是使用一个处于 rs1 字段的、零扩展到 XLEN 位的 5 位立即数(zimm[4:0]) 而不是使用 rs1 整数寄存器的值。对于 CSRRSI 指令和 CSRRCI 指令,如果 zimm[4:0]字段是零, 那么这些指令将不会写 CSR,因此应该不会产生任何由于写 CSR 产生的副作用。对于 CSRRWI 指令,如果 rd=x0,则这条指令将不会读 CSR,且不会导致任何因为 CSR 读而出现的副作用。 某些 CSR,例如指令退休计数器,instret,可能由于指令的执行副作用而被修改。在这 种情形下,如果一条 CSR 访问指令读取了一个 CSR,它读取到的是该指令执行之前的值。如 果一条 CSR 指令写了一个 CSR,这个写的值更新是在该指令执行完之后才发生的。特别地, 一条指令写入 instret 一个值,将是该指令后面一条指令读取到的值(也就是说,由于第一 条指令退休导致的 instret 的增长,是在写入新值之前发生的)。 用于读取 CSR 的汇编语言伪指令 CSRR rd, csr 被编码为 CSRRS rd, csr, x0。用于写 CSR 的 汇编语言伪指令 CSRW csr, rs1 被编码为 CSRRW x0, csr, rs1,而伪指令 CSRWI csr, zimm 被编码 为 CSRRWI x0, csr, zimm。

还有汇编语言伪指令被定义在不需要 CSR 旧值时,用来设置和清除 CSR 中的位: CSRS/CSRC csr, rs1;CSRSI/CSRCI csr, zimm。

定时器和计数器

 

RV32I提供了多个用户级只读的64位计数器,它们被映射到一个12位的CSR地址空间中, 它们可以使用CSRRS指令以32位片段的形式进行访问。 RDCYCLE伪指令读取cycle CSR的低XLEN位,这个计数值是从硬件线程从过去的任意时刻 开始执行以来的时钟周期计数值。RDCYCLEH指令是一条RV32I仅有的指令,它读取同样的计 数值的63-32位。底层的64位计数器在实际使用中应当永远不会溢出。这个周期计数器推进 的速率,与实现和操作系统有关。执行环境应当提供一种手段来判定当前的速率(每秒钟多 少个时钟周期),周期计数器就是按这个时钟周期速率递增的。 RDTIME伪指令读取time CSR的低XLEN位,这个计数值是从过去的任意时刻开始以来的墙 钟实时时间计数值。RDTIMEH指令是一条RV32I仅有的指令,它读取同样的实时时钟计数值 的63-32位。底层的64位计数器在实际使用中应当永远不会溢出。执行环境应当提供一种手 段来判定实时时钟的周期(每tick多少秒),这个周期应当是一个常量。在一个单用户应用程 序中,所有硬件线程的实时时钟必须是同步的,而且误差不能超过实时时钟的一个tick。环 境应当提供一种手段来判定时钟的精度。 RDINSTRET伪指令读取instret CSR的低XLEN位,这个计数值是从硬件线程从过去的任意时 刻开始执行以来的本硬件线程退休(retire)指令的计数值。RDINSTRETH指令是一条RV32I 仅有的指令,它读取同样的指令计数值的63-32位。底层的64位计数器在实际使用中应当永 远不会溢出。 下面的代码序列可以将一个有效的64位周期计数器值写入到x3:x2中,即使这个计数器 在读取它的高低两部分之间产生溢出。

again:

rdcycleh x3

rdcycle x2

rdcycleh x4

bne x3, x4, again

图 2.5:在 RV32 中读取 64 位周期计数器的示例代码

强制这些基本计数器在所有实现中都必须提供,因为它们对于基本的 性能分析、自适应和动态优化来说是必须的,同时允许应用程序在实时流中工作。可能应当提供额外的计数器,以帮助诊断性能问题,而且这些计数器应当 是以较小的代价从用户级应用程序代码中进行访问。  要求这些计数器是 64 位宽度的,即使是在 RV32I 上也是如此。否则 的话,软件就非常难以判定这些值是否已经溢出。对于一个低端的实现,每个 计数器的高 32 位可以实现为使用软件计数器来递增,这个软件计数器的递增, 是由一个自陷处理函数来实现的,每当发生低 32 位溢出时,就触发一次自陷。 上面给出的示例代码显示了如何使用各条 32 位指令,安全地读取完整的 64 位宽度的数值。 在一些应用程序中,同时读取多个计数器的值是很重要的。当运行在一个 多任务下时,一个用户线程在读取这些计数器值时,可能遭受到上下文切换。 用户线程的一个解决方案就是,在读取其他计数器之前和之后,分别读取实时 计数器,用它来判断在这些操作序列过程中是否发生了上下文切换,如果发生 了上下文切换,那么可以重新执行这些读操作。 考虑过添加一个输出锁存 器,允许用户线程对各个计数器值同时进行一个原子性的快照,但是这会增大 用户上下文的大小,特别是实现了一大堆计数器的时候,更是如此。

环境调用和断点

ECALL指令用于向支持的运行环境发出一个请求,这个运行环境通常是一个操作系统。 系统的ABI将定义环境请求的参数是如何传递的,但通常这些参数应当是保存在整数寄存器 中确定的位置。 EBREAK指令被调试器所使用,用来将控制权传送回给调试环境。 ECALL 指令和 EBREAK 指令以前被命名为 SCALL 指令和 SBREAK 指令。这些 指令具有相同的功能和编码,但是被重新命名了,以反映它们可以比调用管理 员级操作系统或者调试器更通用。