目录
乘法与除法
RV32F 和 RV32D:单精度和双精度浮点数
原子操作
压缩指令
向量
乘法与除法
RV32M属于扩展的指令,主要扩展的就是便捷的乘法和除法指令。
除法:
-
商 = (被除数− 余数) ÷ 除数
-
被除数 = 除数× 商+ 余数
-
余数 = 被除数− (商× 除数)
乘法:
-
积 = 被乘数× 乘数
上面的式子需要我们注意,他们是我们指令集工作的一个根本原理。
乘法和除法的指令工作原理如上,可以参考一下。
将两个 32 位数相乘得到的是 64 位的乘积。为了正确地得到一个有符号或无符号的 64 位积,RISC-V 中带有四个乘法指令。
要得到整数 32 位乘积(64 位中的低32 位)就用mul 指令。要得到高32 位,如果操作数都是有符号数,就用 mulh 指令;如果操作数都是无符号数,就用 mulhu 指令;如果一个有符号一个无符号,可以用 mulhsu 指令。在一条指令中完成把 64 位积写入两个 32位寄存器的操作会使硬件设计变得复杂,所以 RV32M 需要两条乘法指令才能得到一个完整的 64 位积。
RV32F 和 RV32D:单精度和双精度浮点数
C语言中的float和double等效到这里就是RV32F 和 RV32D,他们使用32个独立的浮点寄存器。用两组寄存器的主要原因是:处理器在不增加RISC-V 指令格式中寄存器描述符所占空间的情况下使用两组寄存器来将寄存器容量和带宽是乘 2,这可以提高处理器性能。
对于 RV32F 和 RV32D,RISC-V 有两条加载指令(flw,fld)和两条存储指令(fsw,fsd)。他们和lw 和sw 拥有相同的寻址模式和指令格式。添加到标准算术运算中的指令有:(fadd.s,fadd.d,fsub.s,fsub.d,fmul.s,fmul.d,fdiv.s,fdiv.d),RV32F 和 RV32D还包括平方根(fsqrt.s,fsqrt.d)指令。它们也有最小值和最大值指令(fmin.s,fmin.d,fmax.s,fmax.d),这些指令在不使用分支指令进行比较的情况下,将一对源操作数中的较小值或较大值写入目的寄存器。
许多浮点算法(例如矩阵乘法)在执行完乘法运算后会立即执行一条加法或减法指令。因此,RISC-V 提供了指令用于先将两个操作数相乘然后将乘积加上(fmadd.s,fmadd.d)或减去(fmsub.s,fmsub.d)第三个操作数,最后再将结果写入目的寄存器。它还有在加上或减去第三个操作数之前对乘积取反的版本:fnmadd.s,fnmadd.d,fnmsub.s,fnmsub.d。这些融合的乘法 - 加法指令比单独的使用乘法及加法指令更准确,也更快,因为它们只(在加法之后)舍入过一次,而单独的乘法及加法指令则舍入了两次(先是在乘法之后,然后在加法之后)。这些指令需要一条新指令格式指定第 4 个寄存器,称为 R4。图 5.2 和 5.3 显示了 R4 格式,它是R 格式的一个变种。 RV32F 和RV32D 没有提供浮点分支指令,而是提供了浮点比较指令,这些根据两个浮点的比较结果将一个整数寄存器设置为 1 或0:feq.s,feq.d,flt.s,flt.d,fle.s,fle.d。这些指令允许整数分支指令根据浮点数比较指令设置的条件进行分支跳转。
上面两个表格依次指明了我们的我们指令的二进制分布。
这32个寄存器就用来进行浮点操作。
原子操作
任何一个学过操作系统的都知道锁整个东西。那么,他是依赖特定的架构实现的。所以,让我们看看RISC-V是如何实现锁的。
原子操作意味着不会打断操作过程,直到流程做完。加载保留和条件存储保证了它们两条指令之间的操作的原子性。加载保留读取一个内存字,存入目标寄存器中,并留下这个字的保留记录。而如果条件存储的目标地址上存在保留记录,它就把字存入这个地址。如果存入成功,它向目标寄存器中写入 0;否则写入一个非 0 的错误代码。
我们的RV32A存在两种原子操作是出于对场景的特化。编程语言的开发者会假定体系结构提供了原子的比较-交换(compare-and-swap)操作:比较一个寄存器中的值和另一个寄存器中的内存地址指向的值,如果它们相等,将第三个寄存器中的值和内存中的值进行交换。
尽管将这样一条指令加入 ISA 看起来十分有必要,它在一条指令中却需要 3 个源寄存器和 1 个目标寄存器。源操作数从两个增加到三个,会使得整数数据通路、控制逻辑和指令格式都变得复杂许多。(RV32FD 的多路加法(multiply-add)指令有三个源操作数,但它影 响的是浮点数据通路,而不是整数数据通路。)不过,加载保留和条件存储只需要两个源寄 存器,用它们可以实现原子的比较交换。所以,因此存在设计了两种设计思路。
压缩指令
压缩指令!一些ISA选择减小立即数的区域进行压缩,ARM则是整出了直接重新设计指令集的狠活,这些让处理器的设计变得十分的复杂。
RV32C 采用了一种新颖的方法:每条短指令必须和一条标准的 32 位RISC-V 指令一一对应。此外,16 位指令只对汇编器和链接器可见,并且是否以短指令取代对应的宽指令由它们决定。编译器编写者和汇编语言程序员可以幸福地忽略RV32C 指令及其格式,他们能感知到的则是最后的程序大小小于大多数其它 ISA 的程序。
向量
说的就是SIMD!单指令多数据流,从而实现一个并行操作。
把现有的64 位寄存器进行拆分的做法由于其简单性而显得十分诱人。为了使SIMD更快,架构师随后加宽寄存器以同时计算更多部分。由于SIMD ISA 属于增量设计阵营的一员,并且操作码指定了数据宽度,因此扩展 SIMD 寄存器也就意味着要同时扩展 SIMD指令集。将SIMD 寄存器宽度和SIMD 指令数量翻倍的后续演进步骤都让ISA 走上了复杂度逐渐提升的道路,这一后果由处理器设计者、编译器编写者和汇编语言程序员共同承担。
RISC-V则是使用了向量这个概念来进行。向量计算机从内存中中收集数据并将它们放入长的,顺序的向量寄存器中。在这些向量寄存器上,流水线执行单元可以高效地执行运算。然后,向量架构将结果从向量寄存器中取出,并将其并分散地存回主存。向量寄存器的大小由实现决定,而不是像 SIMD 中那样嵌入操作码中。我们将会看到,将向量的长度和每个时钟周期可以进行的最大操作数分离,是向量体系结构的关键所在:向量微架构可以灵活地设计数据并行硬件而不会影响到程序员,程序员可以不用重写代码就享受到长向量带来的好处。此外,向量架构比SIMD架构拥有更少的指令数量。而且,与 SIMD 不同,向量架构有着完善的编译器技术。
RV32V 添加了32 个向量寄存器,它们的名称以 v 开头,但每个向量寄存器的元素个数不同。该数量取决于操作的宽度和专用于向量寄存器的存储大小,而这取决于处理器的设计者。比方说,如果处理器为向量寄存器分配了4096 个字节,则这足以让这些 32 个向量寄存器中有 16 个64 位元素,或者 32 个32 位元素,或者 64 个16 位元素,或128 个 8位元素。
RV32V数据类型和长度与向量寄存器而不是与指令操作码相关联程序在执行向量计算指令之前用它们的数据类型和宽度标记向量寄存器。使用动态寄存器类型会减少向量指令的数量。
动态类型的另一个优点是程序可以禁用未使用的向量寄存器。此功能可以将所有的向量存储器分配给已启用的向量寄存器。比如,假设只启用了两个 64 位浮点类型的向量寄存器,处理器有1024 字节的向量寄存器空间。处理器将这些空间对半分,给每个向量寄存器512 字节(512/8=64 个元素),因此将mvl 设置位 64。因此我们可以看到,mvl 是动态的,但它的值由处理器设置,不能由软件直接改变。
向量Load 和Store 操作的最简单情况是处理按顺序存储在内存中的一维数组。向量Load 用以vld 指令中地址为起始地址的顺序存储的数据来填充向量寄存器。向量寄存器的数据类型确定数据元素的大小,向量长度寄存器 vl 中设置了要取的元素数量。向量store执行 vld 的逆操作。
对于多维数组,某些访问不是顺序的。如果二维数组以行优先序存储,且对列元素进行顺序访问,则相邻列元素之间的地址差正好是行大小。向量架构通过跨步数据传输来支持 vlds 和vsts 数据访问。对于 vlds 与vsts,虽然可以通过将步长设置为元素大小来达到与 vld 和vst 相同的效果,但 vld 和vst 保证了所有的访问都是顺序的,这可以提供更高的内存带宽。另一个原因是,对于常见的按单位步长访问,使用 vld 和vst 可以缩减代码长度,并减少执行的指令数。