RVV向量化编程入门

转自RISC-V vector intrinsic 编程入门指南

本文是为了帮助开发者快速入门 RISC-V 架构下vector 的 intrinsic 编程,首先介绍了RISC-V vector extension 的特性和 intrinsic 编程常见的数据类型与指令接口命名,然后给出一个数组/向量相加的完整例程,介绍C语言的普通实现与intrinsic向量化实现,最后展示了如何获取平头哥相关工具链编译程序并通过qemu模拟器运行。需要说明的是,本文介绍的特性与案例均以玄铁 c906 处理器为基础。

1.vector extension

与ARM CPU支持的 SVE(Scalable Vector Extension) 类似,RISC-V vector extension 可以通过向量化来提升程序运行速度。平头哥玄铁 c906 CPU支持 RISC-V vector extension 0.7.1,新增了32个向量寄存器 v0-v31,下面对 vector extension中重要概念作介绍:
(1) 向量寄存器位宽 vlen:对于玄铁 c906,vlen = 128,即可以并行计算16个8位整数或4个32位浮点数等。
(2) 标准元素宽度 sew:sew = 8/16/32/64b,对于float计算,sew=32b;对于int8计算,sew=8b。
(3) 向量寄存器组 lmul:lmul = 1/2/4/8,多个向量寄存器可以组合在一起形成一个向量寄存器组,一条向量指令就可以对多个向量寄存器进行操作,还能提供更好的执行效率(具体的执行效率取决于执行部件的带宽)。
(4) 向量长度寄存器 vl:vl 寄存器保存一个无符号整数,指定由向量指令更新的元素个数。 在向量指令执行期间,任何具有索引 ≥ vl 的目标向量寄存器组中的元素都被清零。如当 vl = 3 时,对于 float32 的运算,由于vlen=128,只会取向量寄存器中的低96位的3个float32数据进行计算,最高32位将清零,如下图所示。
在这里插入图片描述

2.intrinsic

intrinsic 本质上是底层汇编指令的封装,与手工编写汇编程序相比,intrinsic 编程不需要考虑底层寄存器的分配,对于C语言用户而言更加容易上手,可维护性强,可读性更强,在跨平台的可移植性上也更加友好,配上相关编译器工具链后在性能上几乎可以媲美手工汇编。

2.1 数据类型
根据不同的 sew 和 lmul 组合,RISC-V vector extension intrinsic 基本数据类型格式如下:
v<基本类型>m<向量寄存器组lmul>_t 其中:
基本类型:int8,int16,int32,int64,uint8,uint16,uint32,uint64,float16,float32
向量寄存器组 lmul:1,2,4,8
例如:
vint32m1_t: 1个向量寄存器中存放 vl 个int32 数据, 0 < vl <= 4 (vlen/sewlmul=128/321=4 )
vfloat32m1_t:1个向量寄存器中存放 vl 个float32数据, 0 < vl <=4
vint8m2_t: 2个向量寄存器中存放 vl 个int8数据, 0 < vl <= 32
vfloat16m2_t:2个向量寄存器中存放 vl 个float16数据, 0 < vl <= 16

2.2 intrinsic命名
几乎每一条 RISC-V vector 指令都有其对应的 intrinsic 函数接口,函数名格式如下:
<指令名>_<数据基本类型简写>m<向量寄存器组lmul> 其中:
指令名:vmul.vv (vmul_vv), vadd.vv (vadd_vv) 等
数据基本类型简写:i8,i16,i32,i64,u8,u16,u32,u64,f16,f32
向量寄存器组 lmul:1,2,4,8
例如:

// 2个向量寄存器中各自 vl 个float32数据逐个相乘得到 vl 个float32结果存入1个
向量寄存器中,其中 0 < vl <= 4
vfloat32m1_t vfmul_vv_f32m1(vfloat32m1_t op1, vfloat32m1_t op2, size_t vl)

// 4个向量寄存器两两分组,每组寄存器中各自 vl 个int32数据逐个相加得到 vl 个
int32结果存入2 向量寄存器中,其中  0 < vl <= 8
vint32m2_t vadd_vv_i32m2(vint32m2_t op1, vint32m2_t op2, size_t vl)

关于RVV intrinsic更多数据类型和指令接口可以参考附录中intrinsic手册。

3. 示例程序
本节通过数组相加的例子对 vector 扩展 intrinsic 使用作简单介绍;假设有长度为 ARRAY_LEN 的 float 类型数组 A,B,C,为了实现 C = A + B,有:

3.1 普通标量实现
在循环中对数组 A,B 中的浮点数逐个相加,循环的次数为 ARRAY_LEN 次。

void add(const float *a, const float *b, float *c, size_t length)
{
    for (int i = 0; i < length; i++) {
        c[i] = a[i] + b[i];
    }
}

3.2 intrinsic 向量化实现
在循环中一次对数组 A,B 中的多个浮点数相加:

void add_vec(const float *a, const float *b, float *c, size_t length)
{
    while (length > 0) {
        size_t vl = vsetvl_e32m1(length);       // 设置向量寄存器每次操作的元素个数
        vfloat32m1_t va = vle32_v_f32m1(a, vl); // 从数组a中加载vl个元素到向量寄存器va中
        vfloat32m1_t vb = vle32_v_f32m1(b, vl); // 从数组b中加载vl个元素到向量寄存器vb中
        vfloat32m1_t vc = vfadd_vv_f32m1(va, vb, vl);   // 向量寄存器va和向量寄存器vb中vl个元素对应相加,结果为vc
        vse32_v_f32m1(c, vc, vl);   // 将向量寄存器中的vl个元素存到数组c中
 
        a += vl;
        b += vl;
        c += vl;
        length -= vl;
    }
}

设置 lmul = 1,由于 vlen = 128,向量加法每次最多能操作4个 float 数据,即循环的次数为 ceil(ARRAY_LEN / 4) 次,本例中定义ARRAY_LEN = 11,需循环计算3次,且第3次时剩余元素3个,即 vl=3,计算过程如图1所示。
从上述代码看,在使用 vector intrinsic 实现向量化时,需要手动从指定地址 load 数据到向量寄存器变量中,计算后,同样需要手动将向量寄存器变量中数据 store 回指定地址。相比于普通串行实现,利用 vector intrinsic 实现理论上有接近4倍的加速比,当设置 lmul = 2/4/8 或数据类型是short或者char时,可以取得更高的加速比。

编译和工具链接及使用方法可以看原文链接

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值