正如RV32I基础指令集的一页图形表示。对于每幅图,将有下划线的字母从左到右连接起来,即可组成完整的RV32I 指令集。对于每一个图,集合标志{}内列举了指令的所有变体,变体用加下划线的字母或下划线字符_表示。特别的,下划线字符_表示对于此指令变体不需用字符表示。例如,下图表示了这四个RV32I 指令:slt,slti,sltu,sltiu:
RV32I指令格式
六种基本指令格式,分别是:用于寄存器-寄存器操作的R 类型指令,用于短立即数和访存load操作的I 型指令,用于访存store操作的S型指令,用于条件跳转操作的B类型指令,用于长立即数的U型指令和用于无条件跳转的J型指令。
即使是指令格式也能从一些方面说明RISC-V更简洁的ISA设计能提高提高性能功耗比。首先,指令只有六种格式,并且所有的指令都是32位长,这简化了指令解码。ARM-32,还有更典型的x86-32都有许多不同的指令格式,使得解码部件在低端实现中偏昂贵,在中高端处理器设计中容易带来性能挑战。第二,RISC-V指令提供三个寄存器操作数,而不是像x86-32一样,让源操作数和目的操作数共享一个字段。当一个操作天然就需要有三个不同的操作数,但是ISA只提供了两个操作数时,编译器或者汇编程序程序员就需要多使用一条move(搬运)指令,来保存目的寄存器的值。第三,在RISC-V中对于所有指令,要读写的寄存器的标识符总是在同一位置,意味着在解码指令之前,就可以先开始访问寄存器。在许多其他的ISA中,某些指令字段在部分指令中被重用作为源目的地,在其他指令中又被作为目的操作数(例如,ARM-32 和MIPS-32)。因此,为了取出正确的指令字段,我们需要时序本就可能紧张的解码路径上添加额外的解码逻辑,使得解码路径的时序更为紧张。第四,这些格式的立即数字段总是符号扩展,符号位总是在指令中最高位。这意味着可能成为关键路径的立即数符号扩展,可以在指令解码之前进行。
把带下划线的字母从左到右连接就组成了RV32I指令。花括号{}表示集合中垂直方向上的每个项目都是指令的不同变体。集合中的下划线_意味着不包含这个字母的也是 一个指令名称。例如,左上角附近的符号表示以下六个指令:and or xor andi ori xori。
RISC-V指令格式用生成的立即数值中的位置(而不是通常的指令立即数域中的位置(imm[x])标记每个立即数子域。
说明:B类型和 J类型指令如下所述,分支指令(B类型)的立即数字段在S类型的基础上旋转了1位。跳转指令(J类型)的直接字段在U类型的基础上旋转了12位。因此,RISC-V实际上只有四种基本格式,但我们可以保守地认为它有六种格式。
为了帮助程序员,所有位全部是0是非法的RV32I指令。因此, 试图跳转到被清零的内存区域的错误跳转将会立即触发异常,这可以帮助调试。类似地,所有位全部是1的指令也是非法指令,它将捕获其他常见的错误,诸如未编程的非易失性内存设备、断开连接的内存总线或者坏掉的内存芯片。
为了给ISA扩展留出足够的空间,最基础的RV32I指令集只使用了32位指令字中的编码空间的不到八分之一。架构师们也仔细挑选了RV32I操作码,使拥有共同数据通路的指令的操作码位有尽可能多的位的值是一样的,这简化了控制逻辑。最后,当我们看到,B和J格式的分支和跳转地址必须向左移动1位以将地址乘以2,从而给予分支和跳转指令更大的跳转范围。RISC-V将立即数中的位从自然排布进行了一些移位轮换,将指令信号的扇出和立即数多路复用的成本降低了近两倍,这也简化了低端实现中的数据通路逻辑。
ARM-32指令集12位的立即字段不仅仅是一个常量,而是一个函数的输入,此函数根据12位立即数的输入来产生一个常量:8位被零扩展到全宽度,然后被循环右移。右移的位数是12位立即数中剩余4位的值乘2。设计者希望在12位中编码更多有用的常数来减少执行指令的数量。在大多数指令格式中,ARM-32也将十分宝贵的四位编码空间拿出来专门用于条件执行。这些条件执行指令不仅使用频率低而且增加了乱序处理器的复杂性。
乱序执行处理器:这是一种高速的、流水化的处理器。它们一有机会就执行指令,而不是在按照程序顺序。这种处理器的一个关键特性是寄存器重命名,把程序中的寄存器名称映射到大量的内部物理
寄存器。条件执行的问题是不管条件是否成立,都必须给这些指令中的寄存器分配相应的物理寄存器。但内部物理寄存器的可用性是影响乱序处理器的关键性能资源。
文中列出了RV32I寄存器以及由RISC-V应用程序二进制接口(ABI)所定义的寄存器名称。在示例代码中,我们将使用ABI名称,使它们更容易阅读。为了满足汇编语言程序员和编译器编写者,RV32I有31寄存器加上一个值恒为0的x0寄存器。与之相比,ARM-32只有16个寄存器,x86-32甚至只有8个寄存器。
程序计数器(PC)是ARM-32的16个寄存器之一,这意味着任何改变寄存器的指令都有可能导致分支跳转。PC作为一个寄存器使硬件分支预测变得复杂,因为在典型的ISA中,仅10%-20%的指令为分支指令,而在ARM-32中,任何指令都有可能是分支指令。而分支预测的准确性对于良好的流水线性能至关重要。另外将PC作为一个寄存器也意味着可用的通用寄存器少了一个。
RV32I整数计算
简单的算术指令(add, sub)、逻辑指令(and, or, xor),以及移位指令(sll, srl, sra)和其他ISA差不多。他们从寄存器读取两个32位的值,并将32位结果写入目标寄存器。RV32I还提供了这些指令的立即数版本。和ARM-32不同,立即数总是进行符号扩展,这样子如果需要,我们可以用立即数表示负数,正因为如此,我们并不需要一个立即数版本的sub。
程序可以根据比较结果生成布尔值。为应对这种使用场景下,RV32I提供一个当小于时置位的指令。如果第一个操作数小于第二个操作数,它将目标寄存器设置为1,否则为0。不出所料,对这个指令,有一个有符号版本(slt)和无符号版本(sltu),分别用于处理有符号和无符号整数比较。相应的,上述两条指令也有立即数版本的(slti sltiu)。正如我们将要看到的,虽然RV32I分支指令可以检查两个寄存器之间的所有关系,但一些条件表达式涉及多对寄存器之间的关系。对于这些表达式,编译器或汇编语言程序员可以将slt以及与或异或等逻辑指令组合使用来解决更复杂的条件表达式。加载立即数到高位(lui)将20位常量加载到寄存器的高20位。接着便可以使用标准的立即指令来创建32位常量。这样子,仅使用2条32位RV32I指令,便可构造一个32位常量。向PC高位加上立即数(auipc)让我们仅用两条指令,便可以基于当前PC以任意偏移量转移控制流或者访问数据。将auipc中的20位立即数与jalr(参见下面)中12位立即数的组合,我们可以将执行流转移到任何32位PC相对地址。而auipc加上普通加载或存储指令中的12位立即数偏移量,使我们可以访问任何32位PC相对地址的数据。
有什么不同之处?首先,RISC-V中没有字节或半字宽度的整数计算操作。操作始终是以完整的寄存器宽度。内存访问需要的能量比算术运算高几个数量级。因此低宽度的数据访问可以节省大量的能量,但低宽度的运算不会。ARM-32具有一个不寻常的功能,对于大多数算术逻辑运算中的一个操作数,你可以选择对它进行移位。尽管这些指令的使用频率很低,但它使数据路径和数据通路更加复杂。与此相对的是,RV32I提供了单独的移位指令。 RV32I也不包含乘法和除法,它们包含在可选的RV32M扩展中。与ARM-32和x86-32不同,即使处理器没有添加乘除法扩展,完整的RISC-V软件栈也可以运行,这可以缩小嵌入式芯片的面积。MIPS-32汇编程序可能用一系列移位以及加法指令来替换乘法,以提高性能,这可能会使程序员看到处理器执行了汇编程序中没有的指令,进而造成混淆。RV32I可以忽略了这些特性:循环移位指令和整数算术溢出检测,这两个特性都可以用若干条RV32I指令来实现。
RV32I的Load和Store
除了提供32位字(lw,sw)的加载和存储外,RV32I支持加载有符号和无符号字节和半字(lb,lbu,lh,lhu)和存储字节和半字(sb,sh)。有符号字节和半字符号扩展为32位再写入目的寄存器。即使是自然数据类型更窄,低位宽数据也是被扩展后再处理,这使得后续的整数计算指令能正确处理所有的32位。在文本和无符号整数中常用的无符号字节和半字,在写入目标寄存器之前都被无符号扩展到32位。加载和存储的支持的唯一寻址模式是符号扩展12位立即数到基地址寄存器,这在x86-32中被称为位偏移寻址模式。有什么不同之处?RV32I省略了ARM-32和x86-32的复杂寻址模式。另外,ARM-32提供的寻址模式并非适用于所有数据类型,但RV32I 寻址不会歧视任何数据类型。RISC-V可以模仿某些x86寻址模式。例如,将立即数字段设置为0即与x86中的寄存器间接寻址效果相同。与x86-32不同,RISC-V没有特殊的堆栈指令。将31个寄存器中的某一个作为堆栈指针,标准寻址模式使用起来和压栈(push)和出栈(pop)类似,并且不增加ISA的复杂性。与MIPS-32不同,RISC-V不支持延迟加载(delayed load)。与延迟分支的设计相似,为了更好的适应五级流水线,MIPS-32重新定义了load指令的语义,load上来的数据在load指令两个指令后才可用。但是对于后来出现的更长的流水线,延迟加载带来的收益逐渐消失,因此RISC-V不支持延迟加载。
虽然ARM-32和MIPS-32要求存储在内存中的数据,要按照数据的自然大小进行边界对齐,但是RISC-V没有这个要求。移植旧的代码有时需要未对齐的访问。对于不对齐访问,一种选择是在基础ISA中禁止不对齐访问,然后提供一些单独的指令用于不对齐访问,例如MIPS-32中的Load Word Left和Load Word Right。然而,这会使寄存器访问变得复杂,因为lwl 并且lwr需要对寄存器进行部分写,而不是简单地对寄存器进行完整的写。支持不对齐访问的,另一种方法就是让普通的加载和存储指令支持不对齐访问,这简化了整体设计。
字节序问题:RISC-V选择了小尾端字节序,因为它在商业上占主导地位:所有x86-32系统,Apple iOS谷歌,Android操作系统和微软Windows for ARM都是低字节优先序。由于字节顺序仅在同时以按字访问和按字节访问同一份数据时才会有影响,字节序只会影响很少一部分的程序员。
RV32I条件分支
RV32I可以比较两个寄存器并根据比较结果上进行分支跳转。比较可以是:相等(beq),不相等(bne),大于等于(bge),或小于(blt)。最后两种比较有符号比较,RV32I也提供相应的无符号版本比较的:bgeu和bltu。剩下的两个比较关系(大于和小于等于)可以通过简单地交换两个操作数,即可完成比较。因为x < y表示y > x且x ≥ y表示y ≤ x。
由于RISC-V指令长度必须是两个字节的倍数,分支指令的寻址方式是12位的立即数乘以2,符号扩展它,然后将得到值加到PC上作为分支的跳转地址。PC相对寻址可用于位置无关的代码,简化了链接器和加载器的工作。
有什么不同之处?如上所述,RISC-V去掉了MIPS-32,Oracle SPARC等指令集中被广为诟病的延迟分支特性等。对于条件分支,它还没有像ARM-32和x86-32那样使用条件码。条件码的存在使得大多数指令都需要隐式设置一些额外状态,这使乱序执行的依赖计算复杂化。最后,它省略了x86-32中的循环指令:loop,loope,loopz,loopne,loopnz。
RV32I无条件跳转
跳转并链接指令(jal)具有双重功能。若将下一条指令PC + 4的地址保存到目标寄存器中,通常是返回地址寄存器ra,便可以用它来实现过程调用。如果使用零寄存器(x0)替换ra作为目标寄存器,则可以实现无条件跳转,因为x0不能更改。像分支一样,jal将其20位分支地址乘以2,进行符号扩展后再添加到PC上,便得到了跳转地址。跳转和链接指令的寄存器版本(jalr)同样是多用途的。它可以调用地址是动态计算出来的函数,或者也可以实现调用返回(只需ra作为源寄存器,零寄存器(x0)作为目的寄存器)。Switch和case语句的地址跳转,也可以使用jalr指令,目的寄存器设为x0。有什么不同之处?RV32I避开了错综复杂的程序调用指令,例如x86-32的进入和离开指令,或Intel Itanium,Oracle SPARC和Cadence Tensilica中的寄存器窗口。
RV32I杂项
控制状态寄存器指令(csrrc、csrrs、csrrw、csrrci、csrrsi、csrrwi),使我们可以轻松地访问一些程序性能计数器。对于这些64位计数器, 我们一次可以读取32位。这些计数器包括了系统时间, 时钟周期以及执行的指令数目。在RISC-V指令集中,ecall指令用于向运行时环境发出请求,例如系统调用。调试器使用ebreak指令将控制转移到调试环境。
fence指令对外部可见的访存请求,如设备I/O和内存访问等进行串行化。外部可见指对处理器的其他核心、线程,外部设备或协处理器可见。fence.i指令同步指令和数据流。在执行fence.i指令之前,对于同一个硬件线程,RISC-V不保证用存储指令写到内存指令区的数据可以被取指令取到。
有什么不同之处?RISC-V使用内存映射I/O而不是像x86-32一样,使用in,ins,insb,insw和out,out,outsb等指令来进行I/O。为支持字符串处理,RISC-V实现了字节存取,而不是像x86-32那样实现了 rep,movs,coms,scas,lods等16条特殊的字符串处理指令。
结束语
我们并不是说 RISC-V是第一个拥有这些积极结果的 ISA。事实上, RV32I从RISC-I,它继承了如下这些特性:
⚫ 32位字节可寻址的地址空间
⚫ 所有指令均为 32位长
⚫ 31个寄存器,全部 32位宽,寄存器0硬连线为零
⚫ 所有操作都在寄存器之间(没有寄存器到 内存的操作
⚫ 加载 /存储字加上有符号和无符号加载 /存储字节和半字
⚫ 所有算术,逻辑和移位指令都有立即数版本的指令
⚫ 立即数总是符号扩展
⚫ 仅提供一种数据寻址模式(寄存器+立即数)和PC相对分支
⚫ 无乘法或除法指令
⚫ 一个指令,用于将大立即数加载到寄存器的高位,这样加载32位常量到寄存器只需要两条指令
RISC-V的出现比过去的ISA晚了一 个世纪,这使它的设计者得以实践Santayana的建议 ,即借用之前指令集中好的设计,但不重复它们不好的瑕疵,包括RISC-I指令集中的瑕疵。另外RISC-V基金会将通过可选的指令集扩展的方式缓慢扩展着指令集,以避免出现困扰过去的成功指令集的疯狂的增量发展。
RV32I是否与众不同?
早期的微处理器有单独的浮点运算芯片,所以那些浮点运算指令是可选的。摩尔定律使得我们很快就将所有功能(包括浮点运算)都实现了在同一块芯片上,而且模块化在指令集中逐渐消失。在更简单的处理器中只实现完整的指令集的子集,并利用软件异常来模拟未实现的指令 ,如同数十年前的在IBM 360的44型号和Digital Equipment microVAX。RV32I的不同之处在于完整的软件堆栈只需要RV32I中的基本指令,因此,对于RV32G中未实现的指令, RV32I处理器无需通过软件异常来进行模拟。在这方面,最接近RISC-V的ISA可能是Tensilica Xtensa,它是专为嵌入式应用设计的。它的指令集包含有80条基础指令。并且它的指令集旨在被用户根据自己的需求扩展一些加速指令,以加速其应用程序。与Tensilica Xtensa相比,RV32I具有更简单的基础ISA,并且对超级计算机和微控制器都提供了针对性的指令集扩展。