3. 向量加载存储指令:连续、跨步与索引
加载存储指令,是向量计算的血脉。数据进不来、出不去,再快的计算单元也只能干等。RISC-V 向量指令集提供了三种加载存储模式:连续、跨步和索引。我刚开始接触时也觉得不就是读写内存嘛,但实际调优时才发现,选对模式,性能差距能到 10 倍以上。
3.1 连续加载存储:vle.v / vse.v
这是最基础、最常用的模式。数据在内存中是连续排列的,比如一个数组 float a[1024]。指令会一次性把连续的数据块搬进向量寄存器,或者写回内存。
指令格式很简单:
vle.v vd, (rs1) # 从 rs1 指向的地址加载连续数据到 vd
vse.v vs3, (rs1) # 将 vs3 的数据连续存储到 rs1 指向的地址
这里有个关键点:地址对齐。虽然 RISC-V 向量指令支持非对齐访问,但性能会打折扣。我在项目中遇到过,某次 benchmark 跑出来比预期慢 30%,查了半天发现是数据没对齐。嗯,从那以后我写代码都会先检查地址对齐。
性能要点:连续加载存储的带宽利用率最高,能达到内存理论带宽的 90% 以上。只要数据布局允许,优先用这个模式。
3.2 跨步加载存储:vlse.v / vsse.v
现实中的数据往往不是连续排列的。比如处理一个结构体数组,你只想取其中的某个字段。这时候就需要跨步加载。
指令格式:
vlse.v vd, (rs1), rs2 # 从 rs1 开始,每隔 rs2 字节加载一个元素
vsse.v vs3, (rs1), rs2 # 将 vs3 的元素每隔 rs2 字节存储到内存
跨步加载的典型场景:
- 处理结构体数组中的某个成员
- 矩阵的列访问(行主序矩阵取列)
- 图像处理中的子采样
举个例子,假设有一个结构体:
struct Point {
float x;
float y;
float z;
};
Point points[1024];
你想把所有 x 字段加载到向量寄存器。每个 Point 占 12 字节,跨步就是 12:
# 假设 rs1 指向 points[0].x
li t0, 12 # 跨步 = sizeof(Point) = 12
vlse.v v0, (rs1), t0 # 加载所有 x 字段
我的经验:跨步加载的带宽利用率会随跨步增大而下降。跨步越大,每个 cache line 里有效数据越少。我曾经优化过一个图像处理算法,把跨步从 4 字节改成连续布局后,性能提升了 2 倍。所以,如果可能,尽量在数据布局阶段就考虑向量化。
3.3 索引加载存储:vlxei.v / vsxei.v
这是最灵活的模式。你可以用一个向量寄存器来存储每个元素的地址偏移量。说白了,就是每个元素的地址都是独立的。
指令格式:
vlxei.v vd, (rs1), vs2 # 从 rs1 + vs2[i] 地址加载数据到 vd[i]
vsxei.v vs3, (rs1), vs2 # 将 vs3[i] 存储到 rs1 + vs2[i] 地址
索引加载的典型场景:
- 稀疏矩阵运算
- 查表操作
- 非规则数据访问(比如粒子模拟中每个粒子访问不同的邻居)
你想想看,如果数据访问模式完全随机,连续和跨步都无能为力。索引加载就是为这种情况设计的。但代价也很明显——每次访问都可能触发一次 cache miss。
避坑指南:我曾经在一个稀疏矩阵向量乘的项目里用了索引加载,结果性能还不如标量代码。后来分析发现,索引加载虽然指令数少了,但内存访问完全随机,cache 命中率极低。最后改用分块 + 重排数据的方式才解决问题。所以,索引加载不是万能药,数据局部性差的时候要谨慎。
3.4 三种模式对比
我把三种模式的核心差异整理成了一张表,方便你快速参考:
| 模式 | 指令 | 数据布局要求 | 带宽利用率 | 适用场景 |
|---|---|---|---|---|
| 连续 | vle.v / vse.v | 连续内存块 | 高(>90%) | 数组、缓冲区 |
| 跨步 | vlse.v / vsse.v | 固定间隔 | 中(随跨步下降) | 结构体字段、矩阵列 |
| 索引 | vlxei.v / vsxei.v | 任意地址 | 低(取决于局部性) | 稀疏数据、查表 |
3.5 知识结构图
下面这张图展示了三种加载存储模式的核心逻辑和选择路径:
3.6 实战建议
说了这么多,我总结几条实战中的经验:
- 优先用连续模式。 只要数据能重排成连续布局,就别偷懒。性能差距是数量级的。
- 跨步模式要算好步长。 如果跨步是 cache line 大小的整数倍,性能会更差。我一般会检查跨步是否对齐到 64 字节。
- 索引模式要配合数据重排。 如果索引访问有规律,先对数据排序或分组,把随机访问变成连续访问。
- 用性能计数器验证。 别猜,用工具看 cache miss 率和带宽利用率。我在项目中用
perf stat抓过很多次,数据不会骗人。
小技巧:RISC-V 向量指令支持掩码(mask),可以结合加载存储指令做条件访问。比如只加载满足某个条件的元素。这个后面会详细讲,但你现在可以记住:掩码 + 加载存储 = 更灵活的数据搬运。
三种加载存储模式,说白了就是数据布局和访问模式的匹配问题。连续最快,跨步次之,索引最灵活但最慢。选对模式,你的向量化代码就成功了一半。