3. 向量加载存储指令:vlb/vlh/vlw/vld、vsb/vsh/vsw/vsd、分段加载与存储

好,咱们今天聊点实在的。向量指令集里,加载和存储是最基础、也最容易踩坑的地方。你想想看,数据进不来、出不去,后面再花哨的计算都是白搭。我个人习惯,拿到一个新架构,第一件事就是看它的访存指令怎么设计的——这决定了你能把数据喂多快。

3.1 基础向量加载指令:vlb / vlh / vlw / vld

这四个指令,说白了就是按不同数据宽度从内存往向量寄存器里搬数据。名字里的字母含义很直白:

  • vlb:加载字节(8位),有符号扩展
  • vlh:加载半字(16位),有符号扩展
  • vlw:加载字(32位),有符号扩展
  • vld:加载双字(64位)

它们的指令格式非常统一,我写个例子你感受一下:

// 从内存地址 base_addr 加载 8 个字节到 v1 寄存器
vlb.v v1, (a0)

// 从内存地址 base_addr 加载 4 个半字到 v2 寄存器
vlh.v v2, (a0)

// 从内存地址 base_addr 加载 2 个字到 v3 寄存器
vlw.v v3, (a0)

// 从内存地址 base_addr 加载 1 个双字到 v4 寄存器
vld.v v4, (a0)

这里有个细节要注意——加载的元素个数由 vl(向量长度)寄存器控制。比如 vl=8,vlb 就加载 8 个字节;vl=4,vlh 就加载 4 个半字。嗯,这里要特别提醒一下:vl 的值不能超过 vlenb(向量寄存器字节长度),否则硬件会截断。

我曾经踩过的坑:有一次做图像处理,vl 设成了 16,但 vlenb 只有 8。结果加载的数据只有前 8 个元素是对的,后面全是垃圾值。查了两天才发现是 vl 越界了。所以我的建议是:每次设置 vl 后,顺手读一下 vlenb 确认一下。

3.2 基础向量存储指令:vsb / vsh / vsw / vsd

存储指令和加载指令是对称的,就是把向量寄存器里的数据写回内存:

  • vsb:存储字节(8位)
  • vsh:存储半字(16位)
  • vsw:存储字(32位)
  • vsd:存储双字(64位)
// 将 v1 寄存器的 8 个字节存储到内存
vsb.v v1, (a0)

// 将 v2 寄存器的 4 个半字存储到内存
vsh.v v2, (a0)

// 将 v3 寄存器的 2 个字存储到内存
vsw.v v3, (a0)

// 将 v4 寄存器的 1 个双字存储到内存
vsd.v v4, (a0)

你可能会问:为什么要有这么多宽度?直接都用 vld/vsd 不就行了?其实不是的。我举个例子,你在处理一个字节数组,每个元素只有 8 位。如果用 vld 加载,一次读 64 位,但里面只有 8 位是有效数据,剩下 56 位都是浪费。而且后续还要做位操作才能把数据拆开——效率反而更低。

我的经验:做数据搬运时,尽量匹配数据本身的宽度。处理 char 数组就用 vlb/vsb,处理 int 数组就用 vlw/vsw。这样既省带宽,又省指令。

3.3 分段加载与存储

好了,基础指令讲完了。但实际项目中,数据往往不是连续排列的。比如 RGB 图像,像素数据是 R、G、B 交错存储的。你想单独处理 R 通道,怎么办?

这时候就需要分段加载(segmented load)分段存储(segmented store)了。它们的指令格式是在基础指令上加一个分段数:

// 分段加载:从内存加载 3 个分段(比如 RGB),每个分段 4 个字节
vlseg3w.v v1, (a0)

// 分段存储:将 3 个分段存储到内存
vsseg3w.v v1, (a0)

这个指令干了什么事呢?我画个图帮你理解:

内存布局 R0 G0 B0 R1 G1 B1 R2 G2 B2
v1 寄存器 R0 R1 R2 ...
v2 寄存器 G0 G1 G2 ...
v3 寄存器 B0 B1 B2 ...

看到了吗?vlseg3w.v v1, (a0) 这条指令,自动把交错的数据拆成了三个独立的向量:v1 存 R 通道,v2 存 G 通道,v3 存 B 通道。你后面做滤波、做变换,直接操作 v1、v2、v3 就行,完全不用手动拆包。

分段数 nf 的取值范围:1 到 8。也就是说,你最多可以一次处理 8 个交错的数据流。常见的场景有:

  • nf=2:立体声音频(左声道、右声道)
  • nf=3:RGB 图像
  • nf=4:RGBA 图像或 CMYK 色彩空间

分段存储也是同理。你处理完 R、G、B 三个通道后,一条 vsseg3w.v v1, (a0) 就把它们交错写回内存了。省了多少循环和指针操作?你自己算算看。

3.4 实际项目中的避坑指南

讲到这里,我得跟你分享几个实战中容易出问题的地方:

  1. 地址对齐问题:vld/vsd 要求地址按 8 字节对齐,vlw/vsw 要求 4 字节对齐。不对齐会触发异常。我建议统一按 16 字节对齐,省心。
  2. 分段加载的寄存器占用vlseg3w 会占用 3 个连续的向量寄存器。如果你的代码里寄存器本来就很紧张,记得提前规划好。
  3. vl 与分段数的关系:分段加载时,每个分段加载的元素个数 = vl。比如 vl=4,vlseg3w 总共加载 12 个元素(4 个 R + 4 个 G + 4 个 B)。
  4. 性能陷阱:分段加载虽然方便,但硬件内部需要做数据重排。如果分段数 nf 很大(比如 7 或 8),可能会比手动拆包慢。我建议 nf ≤ 4 时用分段指令,nf 更大时考虑手动处理。
一个小技巧:如果你不确定当前硬件支持哪些分段数,可以读 mvendoridmarchid 寄存器查一下芯片手册。不同厂商的实现差异还挺大的。

好了,向量加载存储这部分就聊到这儿。下一节咱们会讲向量算术指令,到时候你会看到,今天学的这些访存指令,就是给后面的计算指令「喂数据」的。数据喂得好,计算才能跑得快——这个道理,做嵌入式的人都懂。