这是一个使用 RISC‑V 向量扩展(RVV)做“条带化处理”(strip-mining)的示例:从内存按16位元素加载,和一个标量相乘做宽化(16→32位),再把32位结果右移3位,最后以32位存回内存。循环用 vsetvli 根据剩余元素数动态设置 vl,直到处理完所有元素。
逐行解析与要点如下:
-
总体目标
- 输入:a0 = 元素总数,a1 = 源数组地址(int16_t),a2 = 目的数组地址(int32_t)
- 操作:dst[i] = (int32_t(src[i]) * int16_t(x10)) » 3(注意这里是逻辑右移 vsrl,如果需要有符号算术右移应使用 vsra)
- x10 是用作乘法的标量(需事先在其他地方装载)
-
loop:
-
vsetvli a3, a0, e16, m4, ta, ma
- 设置 vtype 为 SEW=16 位、LMUL=4,tail/mask 策略为 agnostic(ta, ma)。
- 根据 avl=a0 与硬件上限 VLMAX 计算本次迭代的 vl,并写入 a3。
- VLMAX = (VLEN / SEW) * LMUL = (VLEN / 16) * 4 = VLEN/4 个16位元素。
- 这一步实现“条带化”:每次处理 min(剩余元素, VLMAX)。
-
vle16.v v4, (a1)
- 按16位元素加载一个向量到寄存器组 v4。
- 因为 LMUL=4,目标是一个寄存器组 v4..v7(4 个寄存器)。
- slli t1, a3, 1
-
add a1, a1, t1
- a3(=vl)个16位元素,每个2字节,源指针前移 2*vl 字节。
-
vwmul.vx v8, v4, x10
- 进行“宽化乘法”:把16位向量 v4 中每个元素与标量 x10 相乘,结果宽化到32位,写入 v8。
- vwmul 是有符号×有符号的宽化乘法;如需无符号请用 vwmulu,混合符号请用 vwmulsu。
- 宽化指令的目的寄存器组 LMUL 会翻倍:源为 m4,则目的是 m8。因此 v8 实际占用 v8..v15。
- 注意:标量 x10 在此按16位解释(按指令的 SEW),超出16位的高位会被截断/按有符号扩展到16位语义。
-
vsetvli x0, x0, e32, m8, ta, ma
- 切换 vtype 到 SEW=32、LMUL=8,以便对刚得到的32位结果进行后续操作与存储。
- 这里使用 rs1=x0、rd=x0 的惯用写法:不改写任何整数寄存器,同时让新 vl 尽量保持与之前相同(规范允许在新 vtype 的 VLMAX 小于旧 vl 时收缩)。在本例中,e32,m8 的 VLMAX = (VLEN/32)*8 = VLEN/4,恰好与之前 e16,m4 的 VLMAX 相同,因此 vl 不会变化。
- 这一步不会覆盖 a3,因此 a3 仍保存着本次迭代的 vl。
-
vsrl.vi v8, v8, 3
- 对32位元素做逻辑右移3位,相当于无符号除以8(舍弃低3位)。
- 如果你的数据是有符号并希望算术右移(保留符号位),应改用 vsra.vi。
-
vse32.v v8, (a2)
- slli t1, a3, 2
-
add a2, a2, t1
- sub a0, a0, a3
-
bnez a0, loop
- 剩余元素数减少 vl,若未清零则继续下一次条带处理。
- 即使初始 a0=0,本循环也安全:第一次 vsetvli 会得到 vl=0,随后的向量访存在 vl=0 时为 no-op,指针不会移动,最后 bnez 不跳转。
关键细节与正确性检查:
-
寄存器组不重叠
- 加载使用 v4..v7(m4),宽化结果使用 v8..v15(m8),两者不重叠,安全。
- RVV 对寄存器组起始编号有对齐要求:m4 需从 4 的倍数开始,m8 需从 8 的倍数开始;v4 和 v8 都满足。
-
vtype 切换与 vl 保持
- 由于 e16,m4 和 e32,m8 的 VLMAX 都是 VLEN/4,使用 vsetvli x0,x0 切换 vtype 时 vl 可保持不变,从而 a3 可继续用于指针更新与剩余计数。
- 如果你在其他情形切换到一个 VLMAX 更小的 vtype,vl 可能被缩小,需要留意。
-
算术语义
- vwmul 是“有符号×有符号”的宽化乘法。如果源数据是无符号的,应改为 vwmulu.vx;若源向量无符号而标量有符号,或相反,使用 vwmulsu。
- vsrl 是逻辑右移。如果需要保留负数的符号位,应使用 vsra。对仅非负数据,vsrl 合理。
-
指针与字节步长
- 源每元素2字节,目的每元素4字节,增量用 a3 计算,能正确覆盖最后一批(部分向量)。
-
尾/掩码策略
- ta, ma(tail/mask agnostic)是高性能常用选择。未被 vl 覆盖的尾部元素在向量寄存器中内容不定义,但因为我们只对 vl 个元素做访存,存储不会越界。
- 如需依赖尾部/掩码关闭元素的旧值,应改用 tu/mu。
-
初值与对齐
- RVV 一般支持非对齐访问,但不同实现可能性能不同甚至不支持;若要求性能或兼容性,尽量让 a1、a2 对齐到自然边界(2字节和4字节)。
- x10 需在进入循环前设好(例如放缩系数)。
-
性能小结
- vsetvli 出现在每次迭代头和中间(切 vtype)。中间这条用 x0,x0 形式是切换 vtype 的最低开销写法,而且保证了 a3 不变。
- 选择 LMUL=4→8 的搭配使两个阶段的 VLMAX 相同,避免了 vl 变化带来的复杂性;这也是常见的“宽化流水”模式。
举个具体规模例子(VLEN=128):
- e16,m4 时 VLMAX = (128/16)4 = 32;e32,m8 时 VLMAX = (128/32)8 = 32。
- 若 a0=100,则 vl 依次为 32, 32, 32, 4;每次源前移 64/64/64/8 字节,目的前移 128/128/128/16 字节,最终处理满100个元素。