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 知识结构总览

下面这张图把本章的指令家族关系梳理清楚了。你可以把它当作快速参考。

向量算术指令家族 向量算术指令 加法:vadd 乘除:vmul / vdiv 极值:vmin / vmax .vv .vx .vi vmul.vv vmul.vx vdiv.vv vmin.vv vmax.vv vmin.vx 关键规律 所有指令都遵循 vd = vs2 op vs1 的格式,操作数顺序统一 变体可用性对比 指令 .vv .vx .vi vadd ✓ ✓ ✓ vsub ✓ ✓ ✗ vmul ✓ ✓ ✓ vdiv ✓ ✓ ✗ vmin/vmax ✓ ✓ ✗

4.6 实战要点总结

最后,我把这章的核心要点整理成几条,方便你快速回顾:

  • 操作数顺序统一:所有指令都是 vd = vs2 op vs1,别记反了
  • 优先用 .vi:常数在 -16~15 范围内时,.vi 最省指令
  • 除法要防 0:除零不会报异常,但结果可能不是你想要的
  • 裁剪用 vmin/vmax:先 vmax 再 vmin,顺序别搞错
  • 乘法注意精度:结果宽度等于输入宽度,需要高精度时用宽乘法

我的建议:刚开始写向量代码时,先把 vadd.vv 和 vmul.vv 练熟。这两条指令是其他所有算术指令的基础。等你对向量数据流有了感觉,再逐步引入 .vx 和 .vi 变体。别一口气吃成胖子。