第4章 基础向量指令集:加载、存储、算术与比较

好,咱们今天正式进入RVV指令集的核心地带。说实话,很多做算法移植的朋友,一开始就被那几十条指令吓住了。其实没那么复杂。你想想看,真正天天用的,翻来覆去就是加载/存储、算术运算、比较这几类。先把它们吃透,后面就顺了。

我个人习惯,学指令集不能光看手册。得一边写代码,一边看反汇编,一边跑仿真。三条腿走路,才记得牢。今天咱们就一条一条过,每一条我都尽量结合项目里的实际场景来讲。

4.1 向量加载/存储指令:vle.v 与 vse.v

向量计算的第一步是什么?把数据从内存搬到寄存器里。这一步要是慢了,后面算得再快也白搭。vle.v(向量加载)和vse.v(向量存储)就是干这个活的。

4.1.1 指令格式与参数

先看个最常用的形式:

vle.v vd, (rs1)        # 从地址rs1加载数据到向量寄存器vd
vse.v vs2, (rs1)       # 将向量寄存器vs2存储到地址rs1

这里有几个关键参数,我列个表方便对照:

参数 含义 我的备注
vd / vs2 目标/源向量寄存器 v0-v31,共32个
rs1 基地址寄存器(通用寄存器) 存的是内存地址
vm 掩码标志(默认 unmasked) 后面章节细讲
宽度后缀 e8/e16/e32/e64 决定每个元素占多少位
我的习惯:写vle.v之前,一定先确认LMUL和vlenb。我吃过亏——有一次LMUL设成m8,结果vlenb是32字节,一次加载256字节,直接把堆栈踩爆了。调试了一下午才发现。

4.1.2 数据宽度与对齐

宽度后缀决定了每个元素的大小。举个例子:

vle32.v v1, (a0)   # 从a0地址加载32位整数,每个元素4字节
vle8.v  v2, (a1)   # 从a1地址加载8位整数,每个元素1字节

这里有个坑,我当年踩过。RVV对地址对齐的要求比你想的宽松——它不要求元素对齐。但不对齐的访问,性能会掉一大截。为什么?因为硬件得拆成多次访问再拼起来。

性能警告:不对齐的向量加载,性能可能下降3-5倍。我在做视频编解码移植时就遇到过,一个不对齐的vle.v,直接把帧率从30fps干到了8fps。排查了半天,最后发现是输入数据起始地址没做64字节对齐。

4.1.3 分段加载与存储

实际项目中,数据往往不是连续排列的。比如RGB图像,三个通道交错存储。这时候就得用分段指令:

vlseg3e32.v v0, (a0)   # 加载3个交错段到v0,v1,v2
vsseg3e32.v v0, (a0)   # 将v0,v1,v2交错存储到内存

这个"seg3"表示分3段。每段对应一个通道。加载完后,v0里全是R值,v1里全是G值,v2里全是B值。省去了手动拆包的麻烦。

4.2 向量算术指令:vadd.vv 与 vmul.vv

数据搬进来了,接下来就是算。向量算术指令是RVV里最常用的一类。咱们先看加法。

4.2.1 向量加法 vadd.vv

vadd.vv vd, vs2, vs1   # vd[i] = vs2[i] + vs1[i]

这条指令做的事情很简单:两个向量寄存器对应位置的元素相加,结果放到目标寄存器。但简单背后有门道。

我记得第一次在项目里用vadd.vv做像素叠加,发现结果不对。查了半天,原来是数据宽度没对上。vs2是e32,vs1是e16,硬件直接报非法指令异常。所以,操作数宽度必须一致。

4.2.2 向量乘法 vmul.vv

vmul.vv vd, vs2, vs1   # vd[i] = vs2[i] * vs1[i]

乘法比加法复杂一些。主要是溢出问题。比如两个32位整数相乘,结果可能超过32位。RVV的处理方式是:保留低32位,高位丢弃。这在很多算法里是不够的。

实战经验:做矩阵乘法时,我一般先用vmul.vv做乘法,再用vadd.vv累加。但累加前必须把结果扩展到64位,否则精度不够。具体做法是先用vsext.vf2做符号扩展,再做加法。

4.2.3 其他常用算术指令

除了加法和乘法,还有几个我经常用的:

  • vsub.vv:向量减法,用法和加法一样
  • vdiv.vv:向量除法,注意除零会触发异常
  • vrem.vv:向量取模,适合做循环边界处理
  • vmin.vv / vmax.vv:向量取最小/最大值,图像处理里常用

4.3 向量比较指令:vmseq.vv

比较指令的作用是生成掩码。掩码是什么?说白了就是一个布尔向量,每个元素是0或1。后面做条件执行全靠它。

4.3.1 指令格式

vmseq.vv vd, vs2, vs1   # vd[i] = (vs2[i] == vs1[i]) ? 1 : 0

注意,这里的目标寄存器vd存的是比较结果,不是原始数据。结果只有0和1两种值。

4.3.2 比较指令家族

RVV提供了完整的比较指令集:

指令 含义 典型用途
vmseq.vv 等于 查找特定值
vmsne.vv 不等于 过滤异常数据
vmslt.vv 小于(有符号) 阈值判断
vmsle.vv 小于等于(有符号) 边界检测
vmsgt.vv 大于(有符号) 峰值查找
vmsge.vv 大于等于(有符号) 范围判断
避坑指南:我曾经在无符号比较时用了vmslt.vv,结果负数被当成很大的正数,比较结果全错了。无符号比较要用vmsltu.vv,带'u'后缀的版本。这个坑,我至少见过三个同事踩过。

4.3.3 比较指令的典型用法

比较指令很少单独用,通常和掩码操作配合。比如:

vmseq.vv v0, v2, v1    # 找出v2中等于v1的元素
vadd.vv v4, v4, v3, v0.t  # 只对匹配的元素做加法

第二行里的v0.t就是掩码。只有v0中为1的位置,加法才会执行。这种条件执行模式,在稀疏数据处理中特别有用。

4.4 本章知识体系

下面这张图,把今天讲的指令分类和关系梳理了一下。我建议你把它存下来,写代码时对照着看:

RVV基础向量指令集知识体系 RVV基础向量指令集知识体系 加载/存储指令 算术指令 比较指令 vle.v / vse.v(连续访问) vlseg.v / vsseg.v(分段访问) 宽度后缀:e8/e16/e32/e64 vadd.vv / vsub.vv vmul.vv / vdiv.vv vmin.vv / vmax.vv vmseq.vv / vmsne.vv vmslt.vv / vmsle.vv vmsgt.vv / vmsge.vv 核心原则 操作数宽度一致 · 地址对齐提升性能 · 比较结果用于掩码控制

4.5 综合示例:向量加法与比较

最后,咱们看一个完整的例子。把前面讲的指令串起来:

# 假设a0指向输入数组A,a1指向输入数组B,a2指向输出数组C
# 计算 C[i] = A[i] + B[i],但只对大于0的元素做

vsetvli t0, a3, e32, m1   # 设置VL,元素宽度32位
vle32.v v0, (a0)          # 加载A
vle32.v v1, (a1)          # 加载B
vmsgt.vv v2, v0, zero     # 找出A中大于0的元素,结果存v2
vadd.vv v3, v0, v1, v2.t  # 只对v2中为1的位置做加法
vse32.v v3, (a2)          # 存储结果

这段代码,我当年在图像阈值增强项目里用过。只对亮度超过阈值的像素做增强,低于阈值的保持不变。用RVV实现,比纯C快了将近8倍。

小结一下:今天讲的这三类指令,是RVV向量化的基石。加载/存储解决数据搬运问题,算术解决计算问题,比较解决条件分支问题。掌握了它们,你已经能写出大部分向量化代码了。剩下的,无非是组合和优化的问题。