时间轴
时间轴
2025-10-30
init
本文介绍了ARM SVE/SVE2可扩展矢量指令集。SVE是面向高性能计算和机器学习的新一代SIMD指令集,并非NEON的简单扩展,其核心是可变矢量长度编程模型(VLA)。文章详细说明了SVE的寄存器结构,包括32个可变长矢量寄存器Z0~Z31、16个预测寄存器P0~P15、首次异常预测寄存器FFR及控制寄存器ZCR_ELx,并阐述了四种特有编程模式:基于预测寄存器的预测指令(合并/零预测)、聚合加载与离散存储、基于预测的循环控制(如WHILELO、INCB、BRKA等指令及PSTATE/NZCV标志位更新),以及基于软件推测的向量分区(通过FFR和LDFF1B处理异常)。此外,还介绍了SVE指令语法、实验环境(如QEMU)以及使用SVE实现memcpy的示例。
参考文档:
SVE
Scalable Vector Extension
- SVE 相关的手册:
- 《ARM Architecture Reference Manual Supplement, The Scalable Vector Extension》
- 《ARM A64 Instruction Set Architecture ARMv9, for Armv9-A architecture profile》
可扩展矢量指令SVE/SVE2
- SVE全称Scalable Vector Extension
- 第一版在ARMv8.2加入,第二版在ARMv9加入
- SVE是针对高性能计算(HPC)和机器学习领域开发的一套全新的矢量指令集,它是下一代SIMD指令集实现,而不是NEON指令集的简单扩展
- SVE指令集中有很多概念与NEON指令集类似,例如矢量,通道,数据元素等
- SVE提出一个全新的概念:可变矢量长度编程模型 (Vector Length Agnostic,VLA)
SVE寄存器
- 32个全新的可变长矢量寄存器Z0~Z31
- 16个可预测寄存器(predicate register)P0~P15
- 首次异常预测寄存器(First-Fault Register,FFR)
- SVE控制寄存器ZCR_ELx
设置矢量寄存器长度
| 寄存器类型 | 名称 | 数量 | 每个寄存器长度 | 说明 |
|---|---|---|---|---|
| Z 寄存器 | Z0–Z31 | 32 个 | VL bits | 向量数据寄存器 |
| P 寄存器 | P0–P15 | 16 个 | VL / 8 bits | 预测寄存器(mask) |
| FFR | FFR | 1 个 | VL / 8 bits | 首次异常预测寄存器(First-Fault Register),predicate 类型 |
矢量长度称为vector length
可变长矢量寄存器

预测寄存器

SVE指令语法
- SVE指令格式由操作代码,目标寄存器,P寄存器和输入操作符组成
1 | LD1D {<Zt>.D}, <Pg>/Z, [<Xn|SP>, <Xm>, LSL #3] |
1 | ADD <Zdn>.<T>, <Pg>/M, <Zdn>.<T>, <Zm>.<T> |
SVE实验环境
早期 Cortex-A 核心
- Cortex-A53 / A55 / A57 / A72
- 仅支持 NEON(128-bit SIMD),不支持 SVE 或 SVE2
QEMU
123456789101112 | qemu-system-aarch64 -m 1024 -cpu max,sve=on,sve256=on -M virt,gic-version=3,its=on,iommu=smmuv3\ -nographic $SMP -kernel arch/arm64/boot/Image \ -append \"$kernel_arg $debug_arg $rootfs_arg $crash_arg $dyn_arg\"\ -drive if=none,file=$rootfs_image,id=hd0\ -device virtio-blk-device,drive=hd0\ --fsdev local,id=kmod_dev,path=./kmodules,security_model=none\ -device virtio-9p-pci,fsdev=kmod_dev,mount_tag=kmod_mount\ $DBG"# 编译,注意必须加上-march=armv8-a+sve参数gcc -g -march=armv8-a+sve -o hello hello_sve.S |

SVE特有编程模式1:预测指令
- SVE指令集为了支持可变长矢量计算提供了预测管理机制(Governing predicate)
- 预测指令会使用预测管理机制来预测矢量寄存器中活跃状态的数据元素有哪些。在预测指令中仅仅处理这些活跃状态的数据元素,对于不活跃的数据元素是不进行处理的。
- 例子:

合并预测与零预测
- 零预测(zeroing predication):在目标矢量寄存器中,不活跃状态数据元素的值填充0
- 合并预测(merging predication):在目标矢量寄存器中,不活跃状态数据元素保持原值不变
零预测

合并预测

SVE特有编程模式2:聚合加载和离散存储
- 支持聚合加载(Gather-load)和离散存储(scatter-store)模式
- 聚合加载和离散存储指的是可以使用矢量寄存器中每个通道的值作为基地址或者偏移量来实现非连续地址的加载和存储
- 传统的NEON指令集只能支持线性地址的加载和存储功能
例子:聚合加载,加载多个离散地址的值

例子:离散存储,存储多个离散地址的值

SVE特有编程模式3:基于预测的循环控制
- 以预测寄存器Pn中活跃状态的数据元素为对象来实现循环控制的
- PSTATE与NZCV状态标志位
- SVE指令集提供如下几组与循环控制相关的指令
- 初始化预测寄存器的指令,例如WHILELO等
- 根据预测约束条件增加数据元素的统计计数,例如INCB等
- 根据 SVE 条件操作码,与跳转指令结合来完成条件跳转功能,例如 B.FIRST 等
- 基于数据元素为对象的比较指令,例如CMPEQ指令等
- 退出循环指令,例如BRKA指令
PSTATE处理器状态和NZCV
- 以数据元素为对象的循环控制方法可以和处理器状态PSTATE有机结合起来
- 当SVE生成一个预测结果时会更新PSTATE的NZCV状态标志位
- SVE指令会根据预测寄存器的结果或者FFR寄存器来更新PSTATE的NZCV状态标志位
- SVE指令也可以根据CTERMEQ/CTERMNE指令来更新PSTATE的NZCV状态标志位

初始化预测寄存器指令
类似C语言的while循环,给定一个初始值和目标值,以一个矢量寄存器包含数据元素的个数为步长,然后以递增或者递减的方式来遍历并初始化预测寄存器中的数据元素

以whilelt为例
1 | whilelt <pd>.<T>, <Rn>, <Rm> |
- <pd>:目标预测寄存器(如 p0、p1)
- <T>:元素类型(如 .b, .h, .s, .d)
- <Rn>:起始索引或计数寄存器
- <Rm>:结束索引或上限寄存器
1 | p[i] = (Rn + i < Rm) ? 1 : 0 |
- 从 Rn 开始,逐个元素地比较索引;
- 只要当前索引还“小于 Rm”,就把对应的
p位设置为 1; - 一旦超出,就置 0。
| 类型 | 元素位宽 | 每步增量(bytes) | 举例 |
|---|---|---|---|
.b | 8-bit | 1 | 1 字节对齐 |
.h | 16-bit | 2 | 2 字节对齐 |
.s | 32-bit | 4 | 4 字节对齐 |
.d | 64-bit | 8 | 8 字节对齐 |
例子
1 | whilelt p0.b, xzr, x2 |
- p0.b 中
.b表示元素(通道)位宽为 8 位 - xzr 是起始值 (0)
- x2 是目标上限值,从低位到高位以递增方式填充,直到达到 x2 或已初始化完预测寄存器中所有元素
SVE条件操作码

根据预测约束条件增加数据元素的统计计数

基于数据元素为对象的比较指令

Break循环指令
- BRKA指令
1 | BRKA <Pd>.B, <Pg>/<ZM>, <Pn>.B |
break after

- BRKB指令
1 | BRKB <Pd>.B, <Pg>/<ZM>, <Pn>.B |

实验2:使用SVE指令实现memcpy_1b()函数

123456789101112131415161718 | .global sve_ld1_test// x0 = dest// x1 = src// x2 = sizesve_ld1_test: mov x3, #0 // p0[i] = ((x3 + i) < x2) ? 1 : 0 whilelt p0.b, x3, x21: // 使用/z避免垃圾值,未加载的元素全部变为 0 ld1b {z0.b}, p0/z, [x1, x3] // 全部写入,不需要/z st1b {z0.b}, p0, [x0, x3] incb x3 whilelt p0.b, x3, x2 b.any 1b ret |
假设VL是256,那么p0中最多可有32个8位宽(B)的元素,incb x3会让x3增加VL对应的字节数,即从0变为32(32个字节)
whilelt p0.b, x3, x2这条指令当x3=32时,p0.b全为0
b.any 表示只要预测寄存器中有一个元素是活跃的就会触发跳转
实验3:使用SVE指令实现memcpy_4b()函数

12345678910111213 | .global sve_ld1_testsve_ld1_test: lsr x2, x2, 2 mov x3, #0 whilelt p0.s, x3, x21: ld1w {z0.s}, p0/z, [x1, x3, lsl 2] st1w {z0.s}, p0, [x0, x3, lsl 2] incw x3 whilelt p0.s, x3, x2 b.any 1b ret |

SVE特有编程模式4:基于软件推测的向量分区
- NEON不支持推测式加载操作(speculative load),SVE支持
- 推测式加载操作遇到的难题:如果在读取过程中某些元素发生内存错误(memory fault)或者访问了无效页面(invalid page),可能很难跟踪究竟是哪个通道的数据读取操作造成的
- SVE引入:
- 首次异常预测寄存器(First-Fault Register,FFR)
- 首次异常加载指令,例如LDFF1B
例子:
1 | LDFF1D Z0.D, P0/Z, [Z1.D] |
以Z1.D寄存器中每个通道的值作为地址,加载对应地址处的元素到Z0

第三个通道是无效地址,直接标记为加载失败而不向CPU报告
SVE/SVE2指令
- SVE 是在 ARMv8.2 加入,SVE2 是在 ARMv9 加入
- SVE/SVE2 指令手册:《ARM A64 Instruction Set Architecture ARMv9, for Armv9-A architecture profile》
- SVE指令集包含了几百条指令,它们可以分成如下几大类
- 加载存储指令以及预取指令
- 向量移动指令
- 整数运算指令
- 位操作指令
- 浮点数运算指令
- 预测操作指令
- 数据元素操作指令
- 如何阅读指令手册:

实验4:案例分析1-使用SVE指令来优化strcmp函数

- 使用SVE指令来优化strcmp()有两个难点:
- 难点1:字符串str1和str2的长度是未知的。在C语言中通过判断字符是否为’\0’来确定字符串的结束。而矢量运算中,SVE加载指令一次装载多个通道的数据。如果装载了字符串结束后的数据,那么会造成非法访问,导致程序出错。
- 难点2:尾数问题
123456789101112131415161718192021222324252627282930313233343536373839404142 | .global strcmp_svestrcmp_sve: ptrue p5.b // p5 = 全1 预测寄存器(以 byte 为元素单位),用于表示“处理所有字节 lane” setffr // 初始化 / 清空 First-Fault Register (FFR),为 fault-first loads 做准备 mov x5, #0 // x5 = byte 偏移索引(从 0 开始),用于对两个字符串的偏移l_loop: ldff1b z0.b, p5/z, [x0, x5] // 从 (x0 + x5) 开始做 fault-first byte 加载到 z0(按 p5 lanes) // 如果发生非法访问等 fault,FFR 会记录已成功加载的 lanes ldff1b z1.b, p5/z, [x1, x5] // 同理,从 (x1 + x5) 加载到 z1(与上面一样使用 fault-first) rdffrs p7.b, p5/z // 读取并清除 FFR,将"已成功加载的 lanes 的掩码"写入 p7(按 p5 lanes), // 使我们知道上面两个 ldff1b 实际成功加载了多少 lanes(若没有 fault,p7==p5) b.nlast l_fault // 如果不是最后块(即发生了fault/只加载了部分 lanes),跳到 l_fault 处理部分加载 // 即若加载被中断/部分完成,需要走故障处理路径 incb x5 // x5 += VL_bytes(按 SVE 的当前向量长度增加偏移),准备下一个完整向量块 cmpeq p0.b, p5/z, z0.b, #0 // p0[i] = (z0[i] == 0) —— 检测 s1 中是否出现 NUL 字节 cmpne p1.b, p5/z, z0.b, z1.b // p1[i] = (z0[i] != z1[i]) —— 检测两个字符串字节是否不同l_test: orrs p4.b, p5/z, p0.b, p1.b // p4 = p0 OR p1;orrs 会同时更新整数条件码(NZ),用于后面的分支 // p4 表示“该 lane 上已到终止条件(NUL 或 不等)” b.none l_loop // 如果 p4 中没有任何位为真(即 none true),继续循环加载下一个向量块l_return: brkb p4.b, p5/z, p4.b // 这里的作用是把 p4 处理成便于提取“第一次发生的位置”的形式。 // 一种常见做法是把要提取的那一位(在当前块中最先出现的 true 位)放到向量的最后位 // 这样可用后续的 lasta 指令直接从该位取出对应的字节。 lasta w0, p4, z0.b // 从 z0 中提取被 brkb 定位的那个字节到 w0(提取成 32 位寄存器) // lasta 的作用是基于 p4 的“最后有效位”提取对应的元素值(zero-extended/或按无符号扩展) lasta w1, p4, z1.b // 同上,从 z1 中提取对应的字节到 w1 sub w0, w0, w1 // 计算差值 w0 = (byte_from_z0) - (byte_from_z1),作为 strcmp 的返回值 ret // 返回(返回值在 w0 中)l_fault: incp x5, p7.b // x5 += 已成功加载的 byte 数(由 p7 指示的真位数,按 byte 为单位) // 这样 x5 指向接下来要继续处理的位置(已经处理过的那部分不用再读) setffr // 重新初始化/清空 FFR,为下一次 fault-first 加载做好准备 cmpeq p0.b, p7/z, z0.b, #0 // 在刚加载成功的那些 lanes 上比较 z0 是否为 0(只在 p7 指示的 lanes 上有效) cmpne p1.b, p7/z, z0.b, z1.b // 在刚加载成功的那些 lanes 上比较 z0 与 z1 是否不等 b l_test // 跳回统一的终止检测(l_test 会做 OR 并决定是否继续或转到返回路径) |

案例5:RGB24转BGR24

案例6:4×4乘积矩阵


和Neon指令不同的地方


总结:上面用到的SVE指令
- whilelt/whilelo类指令
- b.Any类跳转指令
- BRKA和BRKB指令
- LASTA指令
- LD1和ST1指令
- LD3和ST3指令
- FMLA指令
- INCB类指令
- ldff1b 类指令(首次异常加载)
- Setffr和rdffrs类指令
- Cmpeq和cmpne类指令
- Mov类指令
