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(向量寄存器字节长度),否则硬件会截断。
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 位都是浪费。而且后续还要做位操作才能把数据拆开——效率反而更低。
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 实际项目中的避坑指南
讲到这里,我得跟你分享几个实战中容易出问题的地方:
- 地址对齐问题:vld/vsd 要求地址按 8 字节对齐,vlw/vsw 要求 4 字节对齐。不对齐会触发异常。我建议统一按 16 字节对齐,省心。
- 分段加载的寄存器占用:
vlseg3w会占用 3 个连续的向量寄存器。如果你的代码里寄存器本来就很紧张,记得提前规划好。 - vl 与分段数的关系:分段加载时,每个分段加载的元素个数 = vl。比如 vl=4,
vlseg3w总共加载 12 个元素(4 个 R + 4 个 G + 4 个 B)。 - 性能陷阱:分段加载虽然方便,但硬件内部需要做数据重排。如果分段数 nf 很大(比如 7 或 8),可能会比手动拆包慢。我建议 nf ≤ 4 时用分段指令,nf 更大时考虑手动处理。
mvendorid 和 marchid 寄存器查一下芯片手册。不同厂商的实现差异还挺大的。
好了,向量加载存储这部分就聊到这儿。下一节咱们会讲向量算术指令,到时候你会看到,今天学的这些访存指令,就是给后面的计算指令「喂数据」的。数据喂得好,计算才能跑得快——这个道理,做嵌入式的人都懂。