上图给出了基本整数子集的用户可见状态,有31个通用寄存器x1~x31,它们保存了整数数值。寄存器x0是硬件连线的常数0。没有硬件连线的子程序返回地址连接寄存器,但是在一个过程调用中,标准软件调用约定使用寄存器x1来保存返回地址。对于RV32,其x寄存器是32位宽度的,对于RV64,它们是64位宽度的。上图中的XLEN来指明当前x寄存器的宽度(不是32就是64)。还有一个额外的用户可见寄存器:程序计数器pc保存了当前指令的地址。
可用的体系结构寄存器数量,对代码大小、性能和能耗有巨大的影响。虽然有争论说16个寄存器对于一个运行编译代码的整数 ISA 来说足够了,但是在使用 3 地址格式的16位指令中编码16个寄存器,从而实现一个完整的ISA几乎是不可能的。虽然一个2地址格式是可行的,它将增加指令数目并降低效率。我们想避免出现中间的指令长度(例如 Xtensa 的24位指令)来简化基本硬件实现,并且一旦采用32位指令长度,支持32个寄存器就是直截了当的事情。更大的整数寄存器数量也有助于提高高性能代码的性能,可以大量使用循环展开(loop uunrolling)、软件流水线(software pipelining)和cache 分块(cache tiling)。
基于以上这些原因,我们在基本ISA上选择了传统大小32个整数寄存器。动态寄存器使用趋向于被几个频繁访问的寄存器所左右,并且寄存器文件实现可以针对频繁访问的寄存器进行优化,减少访问能耗。可选的压缩16位指令格式大部分时间仅仅访问8个寄存器,并且因此可以提供密集的指令编码, 同时额外的指令集扩展如果需要的话,可以支持大得多的寄存器空间(平坦的或者层次的)。对于资源约束的嵌入式应用,我们定义了RV32E子集,它只有16个寄存器。
在基本ISA中,有四种核心指令格式(R/I/S/U),如上图所示,所有的指令都是固定32位长度的,并且在存储器中必须在4字节边界对齐。当发生一个条件分支或者无条件转移而且目标地址不是对齐到4字节时,将会产生一个指令地址不对齐的异常。如果条件分支没有发生(not taken),那么将不会产生一个取指不对齐异常。在所有格式中,RISC-V ISA将源寄存器(rs1和rs2)和目标寄存器(rd)固定在同样的位置,以简化指令译码。在指令中,立即数被打包,朝着最左边可用位的方向,并且是分配好的,以减少硬件复杂度。特别是所有立即数的符号位总是在指令的第31位,以加速符号扩展电路。
绝大多数整数计算指令对保存在整数寄存器中的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结构被污染。
RV32I定义了几种算术R类操作。所有操作都是读取rs1和rs2寄存器作为源操作数,并把结果写入到寄存器rd中。funct7和funct3字段选择了操作的类型。
ADD和SUB分别执行加法和减法。溢出被忽略,并且结果的低XLEN位被写入目标寄存器rd。SLT和SLTU分别执行符号数和无符号数的比较,如果rs1<rs2,则将1写入rd,否则写入0。注意,SLTU rd,x0,rs2,如果rs2不等于0,则把1写入rd,否则将0写入rd(汇编语言伪指令SNEZ rd,rs)。AND、OR、XOR执行按位逻辑操作。SLL、SRL、SRA分别执行逻辑左移、逻辑右移、算术右移,被移位的操作数是寄存器rs1,移位次数是寄存器rs2的低5位。
NOP指令并不改变任何用户可见的状态,除了使得pc向前推进。NOP被编码为ADDI x0,x0,0。NOP可用于将代码段对齐到对微体系结构有重要作用的地址边界上,或者给内联(inline)代码修改保留空间。虽然有很多种编码可以成为NOP,我们定义了一个正规的NOP编码,允许微体系结构对此进行优化,同时也使得反汇编输出更具可读性。
RV32I提供了两类控制转移指令:无条件跳转和条件分支。RV32I中的控制转移指令,并没有体系结构可见的分支延迟槽。
无条件跳转
跳转并连接(JAL)指令使用了UJ类格式,此处J立即数编码了一个2的倍数的有符号偏 移量。这个偏移量被符号扩展,加到pc上,形成跳转目标地址,跳转范围因此达到±1MB。JAL将跳转指令后面指令的地址(pc+4)保存到寄存器rd中。标准软件调用约定使用x1来作为返回地址寄存器。普通的无条件跳转指令(汇编语言伪指令J)被编码为rd=x0的JAL指令。
间接跳转指令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相对寻址的,并且比分支指令支持大得多的偏移量范围,而且还不会对条件分支预测表造成压力。
至此,以上就基本指令进行了详细分享,有兴趣的坛友回复交流,后续再上其它精彩内容。
更多回帖