4. 向量算术运算:整数与浮点向量运算、归约操作、掩码运算
好,咱们进入第四章。这一章讲的是向量计算的核心——算术运算。说白了,就是怎么让RVV的向量单元真正干起活来。
我刚开始接触RVV时,觉得向量加减乘除跟标量没啥区别。后来才发现,这里面的门道多着呢。尤其是归约和掩码,用好了性能翻倍,用不好反而比标量还慢。
4.1 整数向量运算:基础与陷阱
整数向量运算是最常用的。RVV支持从8位到64位的各种整数类型。咱们先看个简单的加法例子:
// 两个向量相加:v2 = v0 + v1
vadd.vv v2, v0, v1 // 向量-向量加法
vadd.vx v2, v0, x1 // 向量-标量加法
嗯,这里有个细节。RVV的整数运算指令,操作数宽度必须一致。你不能把8位的向量和16位的向量直接加。我在项目中就踩过这个坑——编译器没报错,但结果全错了。
整数乘加运算(MAC)是高性能计算的重头戏。RVV提供了vfmacc、vfmadd等指令。我个人习惯用vfmacc,因为它更符合乘积累加的自然语义:
// 乘积累加:vd[i] = vs1[i] * vs2[i] + vd[i]
vfmacc.vv vd, vs1, vs2
// 融合乘加:vd[i] = vs1[i] * vs2[i] + vs3[i]
vfmadd.vv vd, vs1, vs2
你想想看,这两个指令的区别在哪?vfmacc是累加到目标寄存器,vfmadd是加上第三个操作数。选哪个取决于你的数据流。
4.2 浮点向量运算:精度与性能的平衡
浮点运算比整数复杂得多。RVV支持FP16、FP32、FP64三种精度。我建议能用FP32就别用FP64,除非你真的需要双精度。
为什么?因为FP64的吞吐量只有FP32的一半,甚至更低。我在做图像处理时,把核心计算从FP64改成FP32,速度直接翻倍,精度损失不到0.1%。
| 精度 | 位宽 | 相对吞吐量 | 典型应用 |
|---|---|---|---|
| FP16 | 16位 | 4x | 深度学习推理 |
| FP32 | 32位 | 2x | 通用计算、图形 |
| FP64 | 64位 | 1x | 科学计算、金融 |
浮点运算有个大坑——非规格化数。当浮点数非常接近零时,处理速度会慢几十倍。我曾经在音频处理中遇到这个问题,一个简单的向量加法慢了20倍。排查了半天,才发现是输入数据里有大量接近零的值。
4.3 归约操作:从向量到标量的艺术
归约操作,说白了就是把一个向量“压缩”成一个标量。最常见的归约是求和、求最大值、求最小值。
RVV的归约指令以vred开头。看个例子:
// 向量求和归约:vd[0] = sum(vs1[i])
vredsum.vs vd, vs1, vs2
// 向量最大值归约:vd[0] = max(vs1[i])
vredmax.vs vd, vs1, vs2
注意,归约指令的目标寄存器vd只使用第0个元素,其他元素保持不变。这个设计是为了方便链式归约。
我在项目中遇到过一个问题:归约操作是串行依赖的,没法并行。如果你有多个向量需要归约,别一个一个来。应该先把它们合并成一个更大的向量,一次归约搞定。
4.4 掩码运算:条件执行的利器
掩码运算,是RVV最强大的特性之一。它允许你有条件地执行向量操作,而不需要分支跳转。
掩码寄存器是v0到v7这8个。每个掩码位控制一个向量元素的执行与否。看个例子:
// 只对满足条件的元素执行加法
vadd.vv v2, v0, v1, v0.t // v0.t表示使用v0作为掩码
// 比较生成掩码
vmslt.vv v0, v2, v3 // v0[i] = (v2[i] < v3[i]) ? 1 : 0
嗯,这里要注意。掩码运算的性能开销。虽然避免了分支,但掩码本身也有代价。如果掩码中只有少数几个元素是激活的,那效率反而不如标量。
我曾经在数据过滤中用了掩码,结果比标量还慢。后来改成先压缩再处理,速度就上来了。说白了,掩码适合稀疏但均匀的条件,不适合极度稀疏的情况。
- 掩码激活率 > 50%:放心用掩码
- 掩码激活率 20%-50%:考虑用压缩指令
- 掩码激活率 < 20%:建议用标量处理
4.5 综合案例:向量归一化
咱们把上面的知识串起来,写一个向量归一化的例子。这个例子在机器学习中很常见:
// 输入:v0 存储浮点向量
// 输出:v1 存储归一化后的向量
// 1. 计算平方和
vfmul.vv v2, v0, v0 // v2 = v0 * v0
vfredsum.vs v3, v2, v4 // v3[0] = sum(v2)
// 2. 计算平方根倒数
vfmv.s.f v5, v3[0] // 把标量移到向量寄存器
vfsqrt.v v6, v5 // v6 = sqrt(v3[0])
vfdiv.vv v1, v0, v6 // v1 = v0 / sqrt(sum)
这个例子展示了向量乘、归约、标量-向量混合运算的组合。注意,第2步的vfmv.s.f是把标量结果广播到向量寄存器,这是RVV的常用技巧。
我建议你在实际项目中,把这种常见模式封装成内联函数。这样既保证性能,又提高代码可读性。
这张图把本章的知识点串起来了。从顶层的向量算术运算,分支出整数、浮点、归约三大块,再加上掩码这个横切特性。你写代码时,脑子里要有这张图——知道自己在哪个分支,该用什么指令。
好了,这一章的内容就到这。向量算术运算看起来简单,但每个细节都影响性能。我建议你动手写几个小例子,把整数、浮点、归约、掩码都跑一遍。遇到问题别慌,回头看看这张图,思路就清晰了。