RISC-V Reader 笔记(六)RV32V

RV32V

image-20230708224313312

早期并行数据计算:采用 SIMD 单指令多数据,把一个64位宽寄存器拆成若干个32 16 8 位长度部分并行计算。这种方法前期看起来十分简单诱人。但是后来如果要扩展 SIMD 寄存器宽度,也要复杂化指令集,复杂开销越来越大。
向量操作:把数据取出来放入长长的向量寄存器中,流水线并行运算后从向量寄存器中分别取回到内存。
并且,时钟周期内能进行的最大操作数和向量长度是分离的,不像 SIMD 程序员要考虑怎么分分几位,设计并行硬件的时候不用顾及程序员,程序员也不用重写代码。
向量操作在 ISA 中相比 SIMD 少见多了。

向量计算

基本每一个整数或者浮点运算都有向量版本。.vv 表示操作数都是向量,.vs 表示一个向量一个标量,即一个操作数来自x/f寄存器,一个来自v寄存器。比如 Y=aX+b。.sv 是给非对称运算比如减法 除法用的,表示操作数1是标量 如 Y=a-X。融合的乘法加法则有三个寄存器种类的指示变体。

向量寄存器

向量寄存器不固定大小,这样也减少了生成代码难度和程序员负担,也可以禁用未使用的向量寄存器。比如我们现在只有两个64位浮点类型向量寄存器,处理器有1024字节向量寄存器空间,则分给他俩每个512字节,512/8=64个元素,所以最大向量长度 mvl 为64.

源寄存器和目标寄存器都是有类型的(比如16位浮点 F16,32位浮点,64位浮点……)两者之间长度不兼容处理器会自动进行转换也就是隐式转换。可以通过 vsetdcfg 指令设定向量寄存器的类型。

Load Store

通常用来处理数组。

vld:按地址顺序填充向量寄存器。向量寄存器数据类型确定了元素大小,要取的元素数量存放在长度寄存器 vl 中。

vst:逆操作。

比如 vld v0,0(a0) v0是 x32 向量寄存器,那么 vld 就会从 a0 中的起始地址(假设是1024)开始,依次取1024 1028 1032 1036……(每次4字节)地址中的值直到到达 vl 的限制长度。

有一些结构比如以行优先序存储的二维数组,我们想对列进行顺序访问,就需要有一定的步长设定,而不是单纯的按顺序访问所有元素。可以使用 vlds 和 vsts 因为他们会通过一个寄存器来设定步长。

比如 vlds v0,a0,a1 ,a0 起始地址1024,a1行长64,那么取的位置就依次是1024,1024+64,1024+64*2……直到到达 vl 的限制长度。

第三对操作是针对稀疏数组进行操作的。

vldx v0,a0,v1 a0 起始地址还是1024,v1 中有这些字节索引:16 48 80 160,那么取的位置就是 1024+16 1024+48 1024+80 1024+160.

向量操作期间的并行性

他的并行性和早期的 SIMD 很像,首先取决于处理器每个时钟周期能操作几个多少位数。比如每个时钟周期内能操作4个64位数,那么也可以操作8个32位数,16个16位数,32个8位数。

不过 SIMD 的并行性是 ISA 架构师在设计过程中定好了的,因此寄存器数量增加指令也增加,而且编译器还要修改,相比之下 RVV 就不用改变程序,而且不管硬件结构就都能跑。

条件执行

RVV 的条件执行是每个向量里所有元素分别进行的,结果也是分别赋值给向量中对应的元素的。

RVV 有8个向量谓词寄存器 vpi。他们可以存储向量寄存的结果,也可以进行 vpand vpor vpxor 等运算。

如:vplt.vs vp0,v3,x0 就是判断 v3 中哪些元素是小于0的负数?把 vp0 中对应的这些元素都置1.

然后可以进行一些对部分元素单独进行的操作,比如 add.vv,vp0 v0,v1,v2 就是把前面 vp0 为1的部分替换令v0=v1+v2.

这种操作类似网络里的掩码,vp0 vp1 被规定用作控制向量操作的掩码。(比如我们要操作一个向量中的所有元素,那我们就得用一个全为1的掩码)。

RVV 里有 vpswap 指令可以快速把其他 vpi 的值交换到 vp0 或 vp1. 也可以选择谓词寄存器是否启用,可以通过禁用的方式来清零。

其他向量指令

setvl:把 vl 设置为源操作数和 mvl 中的最小值。如果 mvl 更大,则循环处理 mvl 长度;反之说明源操作数尾部的剩余部分长度小于最大向量长度,处理这一部分即可。

vselect:按第二个源操作数的索引位置,从第一个源操作数中取出一个新的向量。

vselect vedst, vsrc, vindices

比如 vindices 值是 8 0 4 2,则依次取出 vsrc 中第8 0 4 2索引处的元素值替换 vedst 的第0 1 2 3索引处的值。

vmerge:类似 vselect,有两个源操作数,vpo 指代替换的元素来自第一个 vsrc 还是第二个。

vmerge,vpo vdest, vsrc1, vsrc2

比如 vsrc1:1 2 3 4

vsrc2:10 20 30 40

vpo:1 0 0 1

vpo 指代取的数据是:v2的0位,v1的1位,v1的2位,v2的3位。

也就是最终赋值给 vdest 的值是 10 2 3 40。

vextract:从一个向量的中间位置开始取数,放到目标向量的起始位置。

如:vextract vdest, vsrc, start 如果向量长度64,start=32,那么就是从 vsrc 的中间取后32位数放到 vdest 开头32位。

我们可以利用 vextract 来做递归二分操作,比如循环求和所有向量寄存器元素,就可以做6次 vextract。

image-20230829184730094

上例是利用 RV3V 写 DAXPY 程序(y=ax+y,一个函数处理)。

  1. 启用向量寄存器:我们只需要启用两个,给 x y 两个数组用。
  2. 每次循环开始,设定向量长度,y=a*x+y。
  3. 继续遍历或跳出。

简简单单10条左右的指令,一轮循环下来访存3*64次,浮点运算2*64次。每条指令平均有 19 次访存和 13 次运算。

与其他 ISA 向量运算的比较

都拿上例 DAXPY 的例子来进行分析。

MIPS:MSA 寄存器128位宽,每次可以处理2个双精度浮点数。但没有向量长度的寄存器,因此需要单独判断n是否为奇数,奇数最后要多做一次运算。平均每个指令大约有 1 个访存和 0.5 个运算操作。

x86:部分支持 256 位宽,n可以=4. 加载数据时先创建副本并在进入主循环之前进行测试。但是和 MIPS 有相似的问题,需要结尾判断 %4!=0 的部分。每条指令平均有约 2 次访存和 1 次运算。

结语

可以看出向量的一些优势。

如果不用向量,我们就要反复把代码写上好几遍,编程难度大一些。不过新指令程序员记起来也是比较复杂。

RISC-V 的向量运算不受一些向量长度等的影响,比如我把长度修改了,他仍然会自动判断 mvl 并执行,封装的比较好,程序员编码时以及编译器不用考虑这一点的影响。

猜你喜欢

转载自blog.csdn.net/jtwqwq/article/details/132844145