4. SVE2算术运算:矢量加减乘除、融合乘加、归约操作、查表与置换指令
好,咱们进入SVE2最核心的部分——算术运算。说实话,很多做ARMv8优化的朋友刚接触SVE2时,第一反应是“这不就是NEON的宽版吗?”其实不然。SVE2在算术指令上的设计思路,更强调数据级并行与灵活的数据重组。我个人习惯把SVE2的算术指令分成四类:基本算术、融合乘加、归约操作,以及查表置换。下面咱们逐一拆解。
4.1 矢量加减乘除:基础但别小看
先看最基本的加减乘除。SVE2的矢量加减指令和NEON类似,但多了按元素预测的能力。举个例子:
// 矢量加法,受p0谓词控制
ADD Z0.S, P0/M, Z1.S, Z2.S
// 矢量减法,同样带谓词
SUB Z0.S, P0/M, Z1.S, Z2.S
这里有个细节——P0/M表示合并模式(merging)。什么意思呢?就是被掩蔽掉的元素位置,Z0会保留原来的值。还有一种归零模式(zeroing),写法是P0/Z,掩蔽位会直接写0。我在项目中遇到过几次因为模式选错导致的bug,排查了半天才发现是谓词模式没搞对。
乘除指令稍微复杂些。SVE2的乘法支持多种数据类型:
| 指令 | 功能 | 典型场景 |
|---|---|---|
| MUL | 逐元素乘法 | 像素混合、矩阵元素乘 |
| SMULH/UMULH | 高位乘法(有符号/无符号) | 大数乘法、定点数运算 |
| PMUL | 多项式乘法(GF(2)域) | CRC、加密算法 |
| SDIV/UDIV | 有符号/无符号除法 | 归一化、统计计算 |
除法指令在SVE2里是逐元素执行的,延迟比乘法高不少。我曾经在做一个图像归一化项目时,发现除法成了瓶颈。后来改用查表+线性插值替代除法,性能提升了3倍多。所以我的建议是:能避免除法就尽量避免,实在避不开,看看能不能用乘法加移位来近似。
MUL Z0.S, Z1.S, Z2.S[0] 会把Z2的第一个元素广播到所有通道。这在做缩放、偏置时非常实用,省掉了单独的广播指令。
4.2 融合乘加:FMA的威力
融合乘加(FMA)是SVE2的看家本领。它把乘法和加法合并成一条指令,不仅减少指令数,还能保持更高的精度(因为中间结果不截断)。
// 融合乘加:Z0 = Z0 + Z1 * Z2
FMLA Z0.S, P0/M, Z1.S, Z2.S
// 融合乘减:Z0 = Z0 - Z1 * Z2
FMLS Z0.S, P0/M, Z1.S, Z2.S
// 融合乘加,带索引(从Z2的指定通道广播)
FMLA Z0.S, P0/M, Z1.S, Z2.S[3]
这里有个关键点——FMLA和MUL+ADD的区别。FMLA的中间乘积是无限精度的,只在最后做一次舍入。而MUL+ADD会做两次舍入,精度损失更大。我在做科学计算时,曾经因为用了MUL+ADD导致结果偏差,换成FMLA后问题就解决了。
SVE2还引入了复数FMA指令,专门针对复数乘法:
// 复数乘加:Z0 = Z0 + Z1 * conj(Z2)
FCMLA Z0.S, P0/M, Z1.S, Z2.S, #0
这个#0是旋转参数,可以是0、90、180、270度。做FFT或者通信基带处理时,这个指令能省掉大量重排操作。我记得有一次优化一个MIMO检测算法,用FCMLA替换了手动的复数乘法,代码量减少了一半,性能还提升了20%。
4.3 归约操作:从向量到标量
归约操作是把一个向量的所有元素合并成一个标量值。SVE2提供了多种归约指令:
| 指令 | 操作 | 示例 |
|---|---|---|
| ADDV | 向量求和 | ADDV S0, P0, Z0.S |
| SMAXV/UMAXV | 求最大值(有符号/无符号) | SMAXV S0, P0, Z0.S |
| SMINV/UMINV | 求最小值 | SMINV S0, P0, Z0.S |
| ORV | 按位或归约 | ORV S0, P0, Z0.S |
| EORV | 按位异或归约 | EORV S0, P0, Z0.S |
归约操作有个特点——它会把结果写到标量寄存器(比如S0、D0),而不是向量寄存器。这意味着归约后,数据从SIMD域回到了标量域。如果你需要继续做向量运算,得先把标量广播回向量。
我曾经犯过一个错误:在循环里每次迭代都做一次ADDV归约,结果性能惨不忍睹。后来改成每N次迭代归约一次,中间用向量累加,性能就上来了。说白了,归约操作是串行化的,频繁使用会破坏向量化的优势。
4.4 查表与置换:数据重排的艺术
查表和置换是SVE2的杀手锏。它们让你能灵活地重组向量里的数据,这在处理非连续数据时特别有用。
4.4.1 查表指令
SVE2的查表指令有两种:TBL和TBX。
// TBL:从两个向量中查表,索引越界返回0
TBL Z0.B, {Z1.B, Z2.B}, Z3.B
// TBX:从两个向量中查表,索引越界保留原值
TBX Z0.B, {Z1.B, Z2.B}, Z3.B
区别在于越界处理:TBL返回0,TBX保留目标寄存器的原值。我在做AES加密的S-Box替换时,就用TBL一次性处理了16个字节的查表,比逐字节查表快了十几倍。
4.4.2 置换指令
置换指令用于重新排列向量内的元素。SVE2提供了丰富的置换操作:
| 指令 | 功能 | 典型用途 |
|---|---|---|
| UZP1/UZP2 | 奇偶分离 | 解交织、分离实部虚部 |
| ZIP1/ZIP2 | 奇偶交织 | 交织合并、构造复数 |
| TRN1/TRN2 | 矩阵转置 | 矩阵运算、图像转置 |
| REV | 反转元素顺序 | 字节序转换、倒序处理 |
| EXT | 向量拼接提取 | 滑动窗口、卷积 |
举个例子,做FFT时需要做位反转重排,用REV指令可以一次搞定:
// 反转32位元素的顺序
REV Z0.S, P0/M, Z1.S
而EXT指令在做卷积时特别有用。它能把两个向量拼接起来,然后提取中间的一段:
// 从Z1和Z2拼接后的向量中,偏移8字节开始提取
EXT Z0.B, Z1.B, Z2.B, #8
这相当于一个滑动窗口操作。我在优化一个一维卷积算法时,用EXT配合FMA,把卷积的每个输出点都向量化了,性能提升了4倍。
4.5 知识结构总览
为了让你更直观地理解SVE2算术运算的体系,我画了一张结构图:
这张图把SVE2算术运算的四大类及其子指令、性能调优要点都串起来了。你可以把它当作一个快速参考。
4.6 避坑指南
最后,分享几个我踩过的坑:
- 谓词模式搞混:我曾经在循环里用了
P0/Z(归零模式),结果每次迭代都把掩蔽位清零,导致累加结果丢失。后来改成P0/M才正常。记住:累加用合并,初始化用归零。 - FMLA的精度陷阱:虽然FMLA精度高,但如果你需要可复现性(比如科学计算),不同处理器上的FMLA实现可能有细微差异。这时候可能需要用MUL+ADD来保证跨平台一致性。
- TBL索引越界:查表时如果索引值超过了表的大小,
TBL会返回0,TBX会保留原值。我遇到过索引计算错误导致查表结果全0的情况,排查了很久才发现是索引溢出了。 - 置换指令的延迟:虽然置换指令本身延迟低,但数据依赖链会拖慢性能。比如连续做三次UZP,中间没有其他指令,流水线就会空转。我的做法是在置换指令后插入2-3条算术指令,让流水线保持忙碌。