RISCV应该如何打开和使用V扩展命令?

2021-06-18 17:47 来源:电子说

用哪吒D1开发板体验riscv矢量底层编程

1.前言

2.机器模式处理器状态寄存器

3.编译选项支持V扩展

4.4的原则。RISCV向量计算

5.通过实例分析RISCV V扩展的运行机制

6.RVV经历

1.前言

RISCV扩展是向量指令扩展(RVV),在人工智能加速计算的研究中起着关键作用。由于的D1支持rvv扩展(0.7.1,最新版本是0.10),我们将从底层原理的角度实际分析使用的过程。使用多媒体加速指令集可以使计算效率更高,并行计算的特点使得一组数可以同时计算多次,类似arm的NEON等。那么RISCV应该如何打开并使用V扩展指令使计算更高效呢?

下面将通过一个裸机代码结合实战展示riscv rvv的使用。

https://github.com/big magic 123/D1-nezha-bare meta/tree/main/src/2 . vector _ example

2.机器模式处理器状态寄存器

机器模式状态处理寄存器可以查看铁铉C910的用户手册,打开的V扩展位为[23:24]。如果不设置这两个位,在使用V扩展指令时,会出现未定义的异常。

这里需要注意的是,每个RISCV的VS标志不一定是这两位。例如,sifive将在

但是在任何情况下,都需要设置机器状态控制器来启用V扩展指令的支持。

/*如果存在,启用FPU和加速器*/

li t0,MSTATUS _ FS | MSTATUS _ XS |(0x 01800000)

csrs mstatus,t0

在启动代码中,将mstatus设置为0x01800000,以启用v扩展支持。

3.编译选项支持V扩展

默认情况下,平投提供的交叉编译工具链已经支持V扩展的编译。只需在编译选项中打开它。

根据传递给riscv的gcc选项,有v扩展就够了。

-march是指定riscv的模块化指令集选项,目标RISC-V支持的模块化指令集组合可以通过选项指定。例如,以下组合。

rv32i[m][a][f[d]][c]

rv32g[c]

rv64i[m][a][f[d]][c]

rv64g[c]

常与-mabi结合使用。-mabi确定RISCV目标支持的abi函数调用的过程。

4.4的原则。RISCV向量计算

在riscv的V扩展中,共定义了32个寄存器,v0~v31,这32个寄存器各有一个VLEN长度。铁铉C906中定义的长度是128位。

在v扩展的操作中,需要扩展以下寄存器组。

以下是对每个寄存器功能的详细分析。

音速启动

向量起始位置寄存器指定执行向量指令时的起始元素位置,每个向量指令执行后VSTART将清零。

只有当处理器进入陷阱或中断状态时,该寄存器才会被硬件写入。

因此,的向量指令将从vstart中给定的元素号开始执行,并在支持完成后自动变为0。

之所以有这个寄存器,是因为在V扩展指令中每个寄存器都可以分割合并,而不是独立操作。

vxsat

这是矢量定点的饱和标志位,表示定点指令是否必须使输出值饱和才能适应目标格式。

vxrm

向量定点取整模式寄存器,指定定点指令采用的取整模式。

轻链可变区

向量长度寄存器指定向量指令更新目标寄存器的范围,向量指令更新元素序号小于VL的元素,并清除目标寄存器中元素序号大于或等于VL的元素。特别是,当VSTART ”=VL或VL为0时,目标寄存器的所有元素都不会更新。该寄存器在任何模式下都是只读的,但vsetvli、vsetvl和仅故障优先指令可以更新该寄存器的值。

该寄存器的值由vsetvli/vsetvl指令自动设置。

表单验证

VTYPE寄存器指定矢量寄存器组的数据类型和矢量寄存器的元素组成。

根据C910的数据表可以看出

向量长度寄存器VLENB

该寄存器用于指示矢量寄存器的数据位宽,用实际位宽除以8得到的字节数表示。C906的矢量寄存器是128位,所以VLENB值固定为16。该寄存器的位长为64位,在用户模式下为只读。

5.通过实例分析RISCV V扩展的运行机制

以下rvv实际功能

无效测试_v(无效)

{

float a[]={1.0,2.0,3.0,4.0 };

float b[]={1.0,2.0,3.0,4.0 };

float c[]={0.0,0.0,0.0,0.0

};

int len=4;

int i=0;

//inline assembly for RVV 0.7.1

//for(i=0; i《len; i++){c[i]=a[i]+b[i];}

asm volatile(

“mv t4, %[LEN]

“mv t1, %[PA]

“mv t2, %[PB]

“mv t3, %[PC]

“LOOP1:

“vsetvli t0, t4, e32,m1

“sub t4, t4, t0

“slli t0, t0, 2

” //Multiply number done by 4 bytes

“vle.v v0, (t1)

“add t1, t1, t0

“vle.v v1, (t2)

“add t2, t2, t0

“vfadd.vv v2, v0, v1

“vse.v v2, (t3)

“add t3, t3, t0

“bnez t4, LOOP1

:[LEN]“r”(len), [PA]“r”(a),[PB]“r”(b),[PC]“r”(c)

:“cc”,“memory”, “t0”, “t1”, “t2”, “t3”, “t4”,

“v0”, “v1”, “v2”

);

for(i=0; i《len; i++){

printf(“

”);

printf(“%f

”,c[i]);

printf(“

”);

}

}

这里采用的是内联汇编,可以更加深入的分析RVV的运作机制和底层原理。

在riscv中,内联汇编的写法

asm volatile(“nop”);

这样编译器在编译后会生成可以执行的汇编代码。

该函数的功能

for(i=0; i《len; i++){c[i]=a[i]+b[i];}

通过上述分析,通过向量计算,可以一次性计算出上面四次循环加法。

vsetvli t0, t4, e32,m1

vsetvli表示设置每个向量的长度,t4的值表示的是len,也就是4。

e32表示每个元素为32位,m1表示使用1倍数量的向量寄存器。

该条指令相当于把一个向量寄存器(128位)分成四等分,这是一条设置指令,设置vl寄存器。返回值为t0,这里由于是刚好装下4条32位的数字,所以返回值为4。

sub t4, t4, t0

通过查看数组是否计算完成,来进行循环计算,这里t4为0了。

slli t0, t0, 2

往左移动两位,也就是将t0乘以4。这里计算的目的是如果存在很长的数组,可以偏移t0个字节从而指向数组的下个地址。

vle.v v0, (t1)

填充向量寄存器(t1)为a数组,一条指令将数据放到向量寄存器v0中。

add t1, t1, t0

将a数组的起始元素加上16字节(4个元素)的偏移。

vle.v v1, (t2)

填充b数组的数组到向量寄存器v1中。

add t2, t2, t0

将数组b的元素的起始地址偏移16字节,也就是4个元素。

vfadd.vv v2, v0, v1

执行向量加法,将向量的结果保存到向量寄存器v2中。

vse.v v2, (t3)

将向量寄存器中值写回到c数组中。

add t3, t3, t0

将数组c的元素指针偏移4个元素。

bnez t4, LOOP1

直到计算的len长度为0,此时跳出循环计算。

由于此时计算只有4字节,所以一次循环就计算完成了,不用多次计算。

采用向量寄存器的计算,可以把四次循环计算用一次计算就完成。当然这种如果大量计算时,才能体现出更大的优势。

最后的结果如下:

通过对数组的计算

float a[]={1.0,2.0,3.0,4.0};

float b[]={1.0,2.0,3.0,4.0};

float c[]={0.0,0.0,0.0,0.0};

最后c数组的结果

float c[]={2.0,4.0,6.0,8.0};

其理论数据和实际数据一样。

6.RVV使用体验

刚接触到riscv 的 V扩展编程时,很多概念都理解的很模糊,感觉十分的困难,通过一段时间梳理之后,发现和以前mips上接触的mxu或者arm的neno使用上大多数是一样的,就需要去设置使用寄存器的长度,当然这些底层函数如果进行一层封装后,再给用户使用,那才是比较方便的,但是本文只是介绍底层实现的原理,并不多介绍使用的细节。

RVV还有一个特性就是寄存器的扩充,比如D1采用的玄铁C906的核,支持的是32个128位的向量寄存器,也可以将两个或多个向量寄存器拼成一个来使用。这样寄存器的长度更加长,能够同时做到并行计算也就更多。这取决于如何做向量的优化设计。

原文标题:用哪吒D1开发板体验riscv向量底层编程

文章出处:【微信公众号:嵌入式IoT】欢迎添加关注!文章转载请注明出处。

责任编辑:haq

延伸 · 阅读