RVV指令集概览:四大核心指令族
RVV指令集,说白了就是一套让CPU一次性处理一堆数据的“组合拳”。我刚开始接触RVV时,面对几十条指令也懵过。后来发现,只要抓住四个核心家族——向量加载存储、向量算术逻辑、向量归约、向量掩码操作,其他都是变体。
这一章,我带你把每个家族的核心指令过一遍。不追求面面俱到,而是让你知道:每条指令解决什么问题?实际项目中怎么用?
2.1 向量加载存储:数据的“搬运工”
向量加载存储指令,是RVV的“门面”。没有它们,数据进不了向量寄存器,后面再牛的计算也白搭。
我个人习惯把加载存储指令分成三类:
| 指令类别 | 典型指令 | 一句话描述 |
|---|---|---|
| 连续加载/存储 | vle.v / vse.v |
从连续内存地址加载/存储向量 |
| 跨步加载/存储 | vlse.v / vsse.v |
每隔固定间隔取一个元素(比如取矩阵的一列) |
| 索引加载/存储 | vlxei.v / vsxei.v |
按索引数组指定的位置加载/存储(最灵活,也最慢) |
vle.v 的带宽利用率能达到95%以上,而索引加载 vlxei.v 只有60%左右。所以,能用连续加载就别用索引加载,这是向量化优化的第一铁律。
举个例子,加载一个长度为64的整型数组:
// 假设 vl 寄存器保存向量长度,这里设为64
vsetvli t0, a1, e32, m1 // 设置VL=64,元素32位,LMUL=1
vle.v v0, (a0) // 从a0地址连续加载64个int到v0
嗯,这里要注意:vsetvli 是RVV的“调音师”,它决定了向量长度和元素宽度。我见过不少新手上来就写 vle.v,结果忘了设 vsetvli,数据全乱套了。
2.2 向量算术逻辑:计算的“心脏”
算术逻辑指令是RVV最庞大的家族。加减乘除、移位、比较、类型转换……应有尽有。你想想看,一条 vadd.vv 就能同时做几十个加法,这效率,标量CPU只能干瞪眼。
我把常用算术逻辑指令整理成了一张表:
| 操作类型 | 指令格式 | 示例 | 说明 |
|---|---|---|---|
| 向量-向量加法 | vadd.vv vd, vs2, vs1 |
vadd.vv v2, v0, v1 |
v2[i] = v0[i] + v1[i] |
| 向量-标量加法 | vadd.vx vd, vs2, rs1 |
vadd.vx v2, v0, x5 |
v2[i] = v0[i] + x5 |
| 向量乘法 | vmul.vv vd, vs2, vs1 |
vmul.vv v2, v0, v1 |
v2[i] = v0[i] * v1[i] |
| 向量比较(等于) | vmseq.vv vd, vs2, vs1 |
vmseq.vv v0, v1, v2 |
结果写入掩码寄存器v0 |
| 向量移位 | vsll.vv vd, vs2, vs1 |
vsll.vv v2, v0, v1 |
v2[i] = v0[i] << v1[i] |
vmul.vv 做乘法,结果发现精度不对。查了半天,原来是元素宽度设成了 e16(16位),乘法结果溢出截断了。RVV的算术指令不会自动提升精度,你需要手动选择足够宽的LMUL和元素类型。
算术逻辑指令还有一个“隐藏技能”——融合乘加(FMA)。比如 vfmacc.vv 可以一次完成 vd[i] = vs1[i] * vs2[i] + vd[i]。我在做矩阵乘法时,FMA指令能把性能再拉高30%。
2.3 向量归约:把“一群”变成“一个”
归约操作,说白了就是“化整为零”。比如求一个数组的和、找最大值、算逻辑与。RVV用 vred 系列指令搞定这些。
常见的归约指令:
vredsum.vs:向量求和,结果是一个标量vredmax.vs:找最大值vredmin.vs:找最小值vredand.vs:按位与归约
归约指令的格式有点特别:vredsum.vs vd, vs2, vs1。其中 vs2 是源向量,vs1 是标量(通常存初始值),结果写入 vd 的第一个元素。
// 计算数组所有元素的和
vsetvli t0, a1, e32, m1 // 设置VL
vle.v v0, (a0) // 加载数组到v0
vmv.v.x v1, zero // 初始化累加器为0
vredsum.vs v2, v0, v1 // v2[0] = sum(v0[0..VL-1]) + 0
vredsum.vs 的延迟大约是普通 vadd.vv 的3-5倍。所以,尽量减少归约次数,能合并的合并,能分块的分块。
2.4 向量掩码操作:让向量化也能“分支”
向量化最怕什么?分支。标量代码里一个 if-else,到了向量化就头疼。RVV的掩码操作就是来解决这个问题的。
掩码操作的核心思想是:用掩码寄存器(v0)控制哪些元素执行操作,哪些元素跳过。掩码寄存器本身也是一个向量寄存器,每个元素只有0或1。
常用的掩码指令:
| 指令 | 功能 | 示例 |
|---|---|---|
vmerge.vvm |
根据掩码选择两个向量的元素 | vmerge.vvm v2, v0, v1, v0.t |
vmv.v.v |
带掩码的向量移动 | vmv.v.v v2, v1, v0.t |
vmand.mm |
掩码寄存器的按位与 | vmand.mm v0, v1, v2 |
举个例子,实现“如果元素大于0,就加1,否则不变”:
// 假设v0存原始数据
vsetvli t0, a1, e32, m1
vle.v v0, (a0) // 加载数据
vmv.v.i v1, 1 // v1 = 全1
vmsgt.vx v2, v0, 0 // v2掩码 = (v0[i] > 0)
vadd.vv v3, v0, v1, v2.t // 只在掩码为1的位置加1
// v3就是结果:大于0的加1,其他不变
vmin、vmax 这类“无分支”指令替代。实在不行,再用掩码。比如上面的例子,其实可以用 vmax.vx 加 vadd.vv 组合实现,性能更好。
2.5 四大指令族的协同工作
实际项目中,这四类指令很少单独使用。我总结了一个典型的“向量化流水线”:
- 加载:用
vle.v把数据从内存搬到向量寄存器 - 计算:用
vadd.vv、vmul.vv等做核心运算 - 掩码:用比较指令生成掩码,控制条件分支
- 归约:用
vredsum.vs把结果“压缩”成标量 - 存储:用
vse.v把结果写回内存
你想想看,这个流水线里,每一步都有对应的RVV指令。只要掌握了这四大家族,80%的向量化场景你都能搞定。
vdiv.vv)在部分硬件上可能没有实现。写代码前,一定要查目标硬件的指令支持列表。我曾经在移植代码时,发现目标芯片不支持 vfmacc.vv,只好手动拆成乘法和加法,性能直接腰斩。