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 知识结构图

下面这张图展示了三种加载存储模式的核心逻辑和选择路径:

向量加载存储指令选择路径 加载/存储需求 数据布局方式? 连续内存块 固定间隔 任意地址 vle.v / vse.v vlse.v / vsse.v vlxei.v / vsxei.v 性能提示 • 连续模式:带宽利用率最高,优先选择 • 跨步模式:跨步越大,有效带宽越低,考虑数据重排 • 索引模式:最灵活但最慢,仅用于非规则访问

3.6 实战建议

说了这么多,我总结几条实战中的经验:

  1. 优先用连续模式。 只要数据能重排成连续布局,就别偷懒。性能差距是数量级的。
  2. 跨步模式要算好步长。 如果跨步是 cache line 大小的整数倍,性能会更差。我一般会检查跨步是否对齐到 64 字节。
  3. 索引模式要配合数据重排。 如果索引访问有规律,先对数据排序或分组,把随机访问变成连续访问。
  4. 用性能计数器验证。 别猜,用工具看 cache miss 率和带宽利用率。我在项目中用 perf stat 抓过很多次,数据不会骗人。

小技巧:RISC-V 向量指令支持掩码(mask),可以结合加载存储指令做条件访问。比如只加载满足某个条件的元素。这个后面会详细讲,但你现在可以记住:掩码 + 加载存储 = 更灵活的数据搬运。

三种加载存储模式,说白了就是数据布局和访问模式的匹配问题。连续最快,跨步次之,索引最灵活但最慢。选对模式,你的向量化代码就成功了一半。