1. ARM SVE/SVE2向量加载与存储指令概述
在当今高性能计算和机器学习领域,向量化处理已成为提升性能的关键技术。ARM的SVE(Scalable Vector Extension)和其升级版SVE2指令集,通过创新的向量加载和存储机制,为现代处理器提供了强大的数据并行处理能力。与传统的NEON指令集相比,SVE/SVE2最大的突破在于支持可变长度的向量寄存器(128位到2048位),这使得同一套代码可以无缝运行在不同硬件配置的处理器上。
SVE/SVE2的向量加载和存储指令通过谓词寄存器(Predicate Register)实现条件执行,能够灵活控制哪些向量元素需要参与内存操作。这种设计特别适合处理不规则数据结构和条件分支密集的算法。谓词寄存器本质上是一个位掩码,每个位对应向量寄存器中的一个元素,为1表示该元素需要参与操作,为0则被跳过。
提示:SVE/SVE2的向量寄存器命名为Z0-Z31,每个寄存器的实际长度由具体实现决定,可通过CPUID类指令查询。谓词寄存器命名为P0-P15,每个谓词寄存器可控制一个向量寄存器中的所有元素。
2. 内存访问模式与指令分类
2.1 基本内存访问模式
SVE/SVE2的加载存储指令支持多种内存访问模式,每种模式针对不同的数据布局进行了优化:
- 连续访问(Contiguous Access):处理内存中连续存放的数据,如数组、矩阵的行/列等。典型指令包括LD1/ST1系列。
- 结构化访问(Structured Access):处理内存中交错存放的结构化数据,如RGB像素数组、复数数组等。通过LD2/ST2、LD3/ST3、LD4/ST4等指令实现。
- 非连续访问(Non-contiguous Access):处理内存中分散存放的数据,如稀疏矩阵、哈希表等。通过Gather/Scatter指令实现。
- 广播加载(Broadcast Load):将单个内存元素的值复制到向量寄存器的所有位置,如LD1RB等指令。
2.2 指令后缀与数据类型
SVE/SVE2指令通过后缀指定内存元素的访问大小和类型,这是其灵活性的重要体现:
| 后缀 | 数据类型 | 说明 |
|---|---|---|
| B | 无符号字节 | 8位无符号整数 |
| H | 无符号半字/半精度浮点 | 16位数据 |
| W | 无符号字/单精度浮点 | 32位数据 |
| D | 无符号双字/双精度浮点 | 64位数据 |
| SB | 有符号字节 | 8位有符号整数 |
| SH | 有符号半字 | 16位有符号整数 |
| SW | 有符号字 | 32位有符号整数 |
这些后缀可以与不同的指令前缀组合,形成丰富的指令集。例如,LD1SB表示加载有符号字节并进行符号扩展,ST1H表示存储半字数据。
3. 连续内存访问指令详解
3.1 基本连续加载/存储指令
LD1和ST1是SVE/SVE2中最基础的连续内存访问指令,支持多种寻址模式:
// 标量基址+立即数偏移模式 LD1B { Z0.B }, P0/Z, [X1, #0] // 从X1+0地址加载字节到Z0,受P0谓词控制 LD1D { Z0.D }, P0/Z, [X1, #8] // 从X1+8地址加载双字到Z0 // 标量基址+标量索引模式 LD1H { Z0.S }, P0/Z, [X1, X2, LSL #1] // 从X1+X2*2地址加载半字到Z0ST1指令的语法与LD1类似,只是数据流向相反。这些指令都支持谓词控制,只有谓词对应的元素才会参与内存访问。
注意:立即数偏移的范围有限(通常-8到7),而标量索引模式支持更大的地址范围。LSL表示逻辑左移,移位量必须与元素大小匹配(如半字是1,字是2等)。
3.2 非临时(Non-temporal)访问指令
LDNT1和STNT1是非临时加载/存储指令,它们向内存系统提示这些数据不会被立即重用,可以绕过缓存:
LDNT1B { Z0.B }, P0/Z, [X1] // 非临时加载字节 STNT1D { Z0.D }, P0/Z, [X1] // 非临时存储双字这种指令适合处理流式数据或大型数组,可以减少缓存污染,提升整体系统性能。但在小数据量或需要重复访问的场景下反而会降低性能。
3.3 结构化访问指令
结构化访问指令用于处理交错存储的数据,如RGB图像像素(交替存储R、G、B分量):
LD2B { Z0.B, Z1.B }, P0/Z, [X1] // 加载交错的字节对到Z0和Z1 LD3H { Z0.H, Z1.H, Z2.H }, P0/Z, [X1] // 加载交错的半字三元组 LD4W { Z0.S, Z1.S, Z2.S, Z3.S }, P0/Z, [X1] // 加载交错的字四元组对应的ST2/ST3/ST4指令用于存储这类结构化数据。这些指令在图像处理、信号处理等领域非常有用,可以高效地实现数据重组。
4. 非连续内存访问指令
4.1 Gather/Scatter操作原理
Gather和Scatter是SVE/SVE2中最强大的内存访问指令,它们允许向量寄存器中的每个元素从不同的内存地址加载或存储:
- Gather:从一组分散的地址收集数据到连续向量寄存器
- Scatter:将连续向量寄存器中的数据分散存储到一组地址
这种能力使得SVE/SVE2能够高效处理稀疏矩阵、图数据等非规则数据结构。
4.2 基本Gather/Scatter指令
// 标量基址+向量索引模式 LD1D { Z0.D }, P0/Z, [X1, Z1.D, LSL #3] // 从X1+Z1*8地址收集双字 ST1W { Z0.S }, P0/Z, [X1, Z1.S, LSL #2] // 将字分散存储到X1+Z1*4地址 // 向量基址+立即数偏移模式 LD1SB { Z0.D }, P0/Z, [Z1.D, #0] // 从Z1地址收集有符号字节并符号扩展Gather/Scatter指令的性能高度依赖于内存访问模式。完全随机的访问模式会导致较多的缓存缺失,而有一定局部性的访问模式性能会好很多。
4.3 非临时Gather/Scatter
SVE2引入了非临时版本的Gather/Scatter指令,适用于大块稀疏数据处理:
LDNT1W { Z0.S }, P0/Z, [Z1.S, #0] // 非临时收集字数据 STNT1H { Z0.H }, P0/Z, [Z1.H, #0] // 非临时分散存储半字数据5. 特殊加载指令
5.1 广播加载
广播加载指令将单个内存元素的值复制到向量寄存器的所有位置:
LD1RB { Z0.B }, P0/Z, [X1] // 广播无符号字节 LD1RSW { Z0.D }, P0/Z, [X1] // 广播有符号字并符号扩展到双字这种指令在需要将标量值扩展到向量时非常高效,比如矩阵与标量相乘时。
5.2 复制加载
复制加载指令从内存读取一个数据块并复制到向量寄存器的多个位置:
LD1RQB { Z0.B }, P0/Z, [X1] // 加载16字节并复制到向量寄存器 LD1ROH { Z0.H }, P0/Z, [X1] // 加载16半字并复制这些指令在需要创建重复模式时非常有用,比如初始化向量寄存器为特定值。
6. 谓词寄存器加载/存储
除了向量寄存器,SVE/SVE2还支持直接加载和存储谓词寄存器:
LDR P0, [X1] // 从X1地址加载谓词寄存器P0 STR P1, [X1] // 存储谓词寄存器P1到X1地址谓词寄存器的存储格式是紧凑的位图形式,每个字节包含8个谓词位。这种设计使得谓词状态可以高效保存和恢复。
7. SVE2新增指令特性
SVE2在SVE基础上增加了多项增强功能:
- 矩阵加载/存储:支持从内存直接加载数据到ZA矩阵寄存器,加速矩阵运算。
- 连续多向量加载/存储:可以一次性加载/存储多个连续编号的向量寄存器,提升吞吐量。
- 跨步多向量加载/存储:支持以固定间隔访问向量寄存器,方便处理多维数组。
- 瓦片切片操作:针对矩阵运算优化的特殊加载/存储指令。
这些新增指令进一步扩展了SVE2在机器学习、科学计算等领域的应用潜力。
8. 性能优化实践
8.1 内存访问模式优化
- 数据对齐:确保内存访问地址按照元素大小对齐,可以显著提升性能。SVE指令通常要求地址按元素大小对齐。
- 访问局部性:尽量组织数据使内存访问具有空间局部性,减少缓存缺失。
- 预取策略:合理使用PRFH/PRFW等预取指令,提前将数据加载到缓存。
8.2 指令选择策略
- 根据数据类型和大小选择正确的指令后缀,避免不必要的扩展/截断操作。
- 对小数据结构考虑使用结构化加载指令,减少指令数量。
- 对稀疏数据优先使用Gather/Scatter指令,而不是标量加载。
8.3 谓词使用技巧
- 尽量使谓词模式规整,避免完全随机的谓词模式。
- 对条件复杂的场景,可以预先计算谓词寄存器。
- 考虑使用连续谓词模式(如whilelt)生成规整的谓词。
9. 实际应用案例
9.1 图像像素处理
// 处理RGBA像素数组,Alpha通道大于阈值的像素做处理 mov x0, #threshold dup z1.s, w0 // 广播阈值到向量寄存器 ld4b { z0.b, z1.b, z2.b, z3.b }, p0/z, [x1] // 加载RGBA像素 cmpgt p1.b, p0/z, z3.b, z1.b // 创建Alpha>threshold的谓词 // 对p1谓词选中的像素进行处理9.2 稀疏矩阵向量乘法
// z0: 结果向量 // x1: 行指针数组 // x2: 列索引数组 // x3: 值数组 // x4: 向量x地址 mov z0.d, #0 // 清零结果向量 ld1d z1.d, p0/z, [x1] // 加载行指针 ld1d z2.d, p0/z, [x2] // 加载列索引 ld1d z3.d, p0/z, [x3] // 加载矩阵值 ld1w z4.d, p0/z, [x4, z2.d, lsl #2] // 收集向量x的值 fmad z0.d, p0/m, z3.d, z4.d // 乘加运算9.3 数据过滤与重组
// 过滤数组中小于0的元素并压缩存储 ld1w z0.s, p0/z, [x1] // 加载数组 cmplt p1.s, p0/z, z0.s, #0 // 创建小于0的谓词 compact z1.s, p1, z0.s // 压缩符合条件的元素 st1w z1.s, p1, [x2] // 存储结果这些案例展示了SVE/SVE2指令在各种场景下的强大表现力,通过合理使用向量加载/存储指令,可以显著提升数据处理效率。