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倍多。所以我的建议是:能避免除法就尽量避免,实在避不开,看看能不能用乘法加移位来近似。

小技巧:SVE2的MUL指令支持按元素广播,比如 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%。

性能调优要点:FMA指令的吞吐量通常比单独的MUL和ADD高。在ARMv9处理器上,一条FMLA可以做到每周期2-4条(取决于向量长度和数据类型)。所以,能用FMA的地方,尽量别拆开写。

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次迭代归约一次,中间用向量累加,性能就上来了。说白了,归约操作是串行化的,频繁使用会破坏向量化的优势。

注意:归约指令的延迟通常比普通算术指令高。以ADDV为例,它需要把向量所有元素加起来,内部是一个树形加法器,延迟大约是普通ADD的2-3倍。所以,不要在热循环里频繁调用归约指令。

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倍。

实战经验:置换指令的延迟通常只有1-2个周期,但数据依赖会影响流水线。比如连续做两次UZP,中间如果没有其他指令,可能会造成停顿。我的习惯是在置换指令后插入几条算术指令,让流水线保持饱满。

4.5 知识结构总览

为了让你更直观地理解SVE2算术运算的体系,我画了一张结构图:

SVE2算术运算知识体系 基本算术 融合乘加 归约操作 查表与置换 加减乘除 ADD / SUB MUL / SDIV SMULH / UMULH PMUL(多项式) FMA系列 FMLA / FMLS FCMLA(复数) 带索引广播 高精度舍入 归约指令 ADDV(求和) SMAXV / SMINV ORV / EORV 结果→标量寄存器 查表与置换 TBL / TBX(查表) UZP / ZIP(交织) TRN / REV(转置) EXT(滑动窗口) 核心原则 能用FMA不用MUL+ADD | 归约操作要节制 | 置换指令配合算术指令保持流水线饱满 性能调优要点 • 谓词模式选择:合并模式(M) vs 归零模式(Z),影响数据依赖 • 除法优化:尽量用乘法+移位或查表替代 • FMA吞吐量:每周期2-4条,充分利用流水线 • 归约频率:避免每次迭代都归约,采用批量归约策略 • 置换指令:延迟低但数据依赖敏感,需交错安排

这张图把SVE2算术运算的四大类及其子指令、性能调优要点都串起来了。你可以把它当作一个快速参考。

4.6 避坑指南

最后,分享几个我踩过的坑:

  • 谓词模式搞混:我曾经在循环里用了P0/Z(归零模式),结果每次迭代都把掩蔽位清零,导致累加结果丢失。后来改成P0/M才正常。记住:累加用合并,初始化用归零。
  • FMLA的精度陷阱:虽然FMLA精度高,但如果你需要可复现性(比如科学计算),不同处理器上的FMLA实现可能有细微差异。这时候可能需要用MUL+ADD来保证跨平台一致性。
  • TBL索引越界:查表时如果索引值超过了表的大小,TBL会返回0,TBX会保留原值。我遇到过索引计算错误导致查表结果全0的情况,排查了很久才发现是索引溢出了。
  • 置换指令的延迟:虽然置换指令本身延迟低,但数据依赖链会拖慢性能。比如连续做三次UZP,中间没有其他指令,流水线就会空转。我的做法是在置换指令后插入2-3条算术指令,让流水线保持忙碌。