4. 向量算术指令基础:vadd.vv、vadd.vx、vadd.vi、vsub、vmul、vdiv、vmin、vmax
算术指令是向量计算的基石。说白了,你写任何向量程序,八成以上的时间都在跟加减乘除打交道。RISC-V 向量扩展把这套指令设计得相当规整——一条指令,一次操作,一组元素并行完成。
我个人习惯把向量算术指令分成三类:向量-向量、向量-标量、向量-立即数。今天咱们就把这三类讲透。
4.1 加法指令:vadd 家族
加法指令有三个变体,分别对应三种操作数来源。我刚开始接触时觉得这有点啰嗦,后来发现每个变体都有它的用武之地。
| 指令 | 操作数1 | 操作数2 | 典型场景 |
|---|---|---|---|
vadd.vv |
向量寄存器 vs2 | 向量寄存器 vs1 | 两路向量逐元素相加 |
vadd.vx |
向量寄存器 vs2 | 通用寄存器 rs1 | 向量每个元素加同一个标量 |
vadd.vi |
向量寄存器 vs2 | 5位立即数 imm | 加一个编译期已知的小常数 |
关键点:三种变体的目的操作数都是 vd,源操作数都是 vs2。这个统一格式让你写代码时不用来回切换思维。
看个实际例子。假设你要给数组每个元素加 5:
# 方式一:vadd.vi(推荐,立即数范围 -16 ~ 15)
vadd.vi vd, vs2, 5
# 方式二:vadd.vx(标量在寄存器中)
li t0, 5
vadd.vx vd, vs2, t0
# 方式三:vadd.vv(需要另一个向量存满5)
vadd.vv vd, vs2, vtemp # vtemp 每个元素都是5
你想想看,哪种最省事?当然是 vadd.vi。一条指令搞定,不占寄存器,不产生额外数据移动。
我的经验:在循环中加常数时,优先用 .vi 变体。如果常数超过 5 位有符号范围(-16~15),再用 .vx。我曾经见过有人全程用 .vx,结果多占了一个通用寄存器,还多了一条 li 指令——其实完全没必要。
4.2 减法与乘法:vsub 和 vmul
减法只有 vsub.vv 和 vsub.vx 两种变体,没有立即数版本。为什么?因为减法可以转化为加法:a - b = a + (-b)。如果你需要减一个常数,用 vadd.vi 加负数就行。
乘法同样有三个变体:vmul.vv、vmul.vx、vmul.vi。不过要注意,乘法指令的结果宽度和输入宽度相同。比如两个 32 位整数相乘,结果只保留低 32 位。
注意溢出:RISC-V 向量乘法不会自动提升精度。如果你需要 32 位乘 32 位得 64 位的结果,得用 vwmul 系列(宽乘法),这个我们后面章节会讲。
我记得有一次做音频滤波,需要把每个采样点乘以一个系数。当时图省事直接用了 vmul.vx,结果系数是浮点数……嗯,向量扩展的乘法指令只支持整数。浮点乘法得用 vfmul 系列。这个坑我替你们踩过了。
4.3 除法:vdiv 和特殊值处理
除法指令分有符号和无符号两种:
vdiv.vv/vdiv.vx— 有符号整数除法vdivu.vv/vdivu.vx— 无符号整数除法
除法和乘法不同,它没有立即数版本。原因很简单:除法的立即数优化空间不大,而且硬件实现复杂。
关于除法的特殊值处理,RISC-V 向量扩展有明确规定:
| 场景 | 结果 |
|---|---|
| 除以 0 | 全 1(-1 或最大值) |
| 溢出(如 INT_MIN / -1) | INT_MIN(保持原值) |
| 正常除法 | 向零取整 |
避坑指南:我曾经在图像处理流水线里用 vdiv 做归一化,没检查除数向量中是否有 0。结果某些像素值直接变成了 0xFFFFFFFF,整张图出现了奇怪的白色噪点。排查了半天才发现是除零问题。所以,用除法前一定要先做除数检查,或者用 vmsne 指令生成掩码来保护。
4.4 极值指令:vmin 和 vmax
这两条指令用来找两个向量对应元素的最小值和最大值。同样有符号和无符号两个版本:
vmin.vv/vminu.vv— 逐元素取小vmax.vv/vmaxu.vv— 逐元素取大
它们也有 .vx 变体,但没有 .vi 版本。为什么?因为极值操作通常需要动态比较,很少用固定常数。
你可能会问:vmin 和 vmax 有什么用?我举几个实际场景:
- 数据裁剪:把向量元素限制在 [0, 255] 范围内,用
vmax和vmin组合实现 - 绝对值计算:
vmax(v, -v)可以快速求绝对值 - 查找极值:配合归约指令
vredmax找到整个向量的最大值
我的习惯:做数据裁剪时,我通常先 vmax 再 vmin。顺序别搞反了——先取大再取小,才能保证下限和上限都生效。如果先取小再取大,下限会被上限覆盖掉。
4.5 知识结构总览
下面这张图把本章的指令家族关系梳理清楚了。你可以把它当作快速参考。
4.6 实战要点总结
最后,我把这章的核心要点整理成几条,方便你快速回顾:
- 操作数顺序统一:所有指令都是
vd = vs2 op vs1,别记反了 - 优先用 .vi:常数在 -16~15 范围内时,
.vi最省指令 - 除法要防 0:除零不会报异常,但结果可能不是你想要的
- 裁剪用 vmin/vmax:先
vmax再vmin,顺序别搞错 - 乘法注意精度:结果宽度等于输入宽度,需要高精度时用宽乘法
我的建议:刚开始写向量代码时,先把 vadd.vv 和 vmul.vv 练熟。这两条指令是其他所有算术指令的基础。等你对向量数据流有了感觉,再逐步引入 .vx 和 .vi 变体。别一口气吃成胖子。