时间轴

2025-10-30

init

参考文档:

SVE

Scalable Vecotr Extension

  • SVE相关的手册:
    • <>
    • <>

可扩展矢量指令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 predicate 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)

矢量长度称为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 / A76 / A77 / A78
    • 仅支持 NEON(128-bit SIMD),不支持 SVE 或 SVE2

QEMU

1
2
3
4
5
6
7
8
9
10
11
12
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指令

SVE特有编程模式1:预测指令

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

Governing predicate

合并预测与零预测

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

零预测

零预测

合并预测

合并预测

SVE特有编程模式2:聚合加载和离散存储

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

例子:聚合加载,加载多个离散地址的值

聚合加载,加载多个离散地址的值

离散存储

离散存储

SVE特有编程模式3:基于预测的循环控制

  • 预测寄存器Pn活跃状态的数据元素为对象来实现循环控制的
  • PSTATE与NVCZ状态标志位
  • SVE指令集提供如下几组与循环控制相关的指令
    • 初始化预测寄存器的指令,例如WHILELO等
    • 根据预测约束条件增加数据元素的统计计数,例如INCB等
    • 根据SVE条件操作码,与跳转指令结合来完成条件跳转功能,例如B.FIRST等
    • 基于数据元素为对象的比较指令,例如CMPEQ指令等
    • 退出循环指令,例如BRKA指令

PSTATE处理器状态和NCZV

  • 以数据元素为对象的循环控制方法可以和处理器状态PSTATE有机结合起来
    • 当SVE生成一个预测结果时会更新PSTATE的NCVZ状态标志位
    • SVE指令会根据预测寄存器的结果或者FFR寄存器来更新PSTATE的NCVZ状态标志位
    • SVE指令也可以根据CTERMEQ/CTERMNE指令来更新PSTATE的NCVZ状态标志位

处理器状态标志位与SVE

初始化预测寄存器指令

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

初始化预测寄存器指令

以whilelt为例

1
whilelt  <pd>.<T>, <Rn>, <Rm>
  • \:目标预测寄存器(如 p0、p1)
  • \:元素类型(如 .b, .h, .s, .d)
  • \:起始索引或计数寄存器
  • \:结束索引或上限寄存器
1
p[i] = (Rn + i*sizeof(T)/8 < 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是起始值
  • x2是目标值,从低到高以递增方式达到x2为止或者已经初始化完预测寄存器中所有值

SVE条件操作码

SVE条件操作码

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

增加数据元素的统计计数

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

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

Break循环指令

  • BRKA指令
1
BRKA <Pd>.B, <Pg>/<ZM>, <Pn>.B

break after

BRKA指令

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

BRKB指令

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

实验2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.global sve_ld1_test
// x0 = dest
// x1 = src
// x2 = size
sve_ld1_test:
mov x3, #0
// p0[i] = ((x3 + i) < x2) ? 1 : 0
whilelt p0.b, x3, x2
1:
// 使用/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从0变为32(32个字节)

whilelt p0.b, x3, x2这条指令当x3=32时,p0.b全为0

b.any表示只要矢量寄存器中有一个元素是活跃的都会触发跳转

实验3:使用SVE指令实现memcpy_4b()函数

实验3

1
2
3
4
5
6
7
8
9
10
11
12
13
.global sve_ld1_test
sve_ld1_test:
lsr x2, x2, 2
mov x3, #0
whilelt p0.s, x3, x2
1:
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 predicate Register,FFR)
    • 首次异常加载指令,例如LDFF1B

例子:

1
LDFF1D Z0.D, P0/Z, [Z1.D]

以Z1.D寄存器中每个通道的值为基地址,加载其地址对应的元素到Z0

以Z1.D寄存器中每个通道的值为基地址,加载其地址对应的元素到Z0

第三个通道是无效地址,直接标记为加载失败而不向CPU报告

SVE/SVE2指令

  • SVE是在ARMv8.2加入,SVE2是在ARMv9加入
  • SVE/SVE2指令手册:\<\>
  • SVE指令集包含了几百条指令,它们可以分成如下几大类
    • 加载存储指令以及预取指令
    • 向量移动指令
    • 整数运算指令
    • 位操作指令
    • 浮点数运算指令
    • 预测操作指令
    • 数据元素操作指令
  • 如何阅读指令手册:

三种

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

实验4

  • 使用SVE指令来优化strcmp()有两个难点:
    • 难点1:字符串str1和str2的长度是未知的。在C语言中通过判断字符是否为’\0’来确定字符串的结束。而矢量运算中,SVE加载指令一次装载多个通道的数据。如果装在了字符串结束后的数据,那么会造成非法访问,导致程序出错。
    • 难点2:尾数问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
.global strcmp_sve
strcmp_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_retrun:
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

案例5

案例6:4×4乘积矩阵

案例6

案例6

和Neon指令不同的地方

和Neon指令不同的地方

Neon指令不同的地方

总结:上面用到的SVE指令

  • whilelt/whilelo类指令
  • b.Any类跳转指令
  • BRKA和BRKB指令
  • LASTA指令
  • LD1和ST1指令
  • LD3和ST3指令
  • FMLA指令
  • INCB类指令
  • ld1ff1b类指令
  • Setffr和rdffrs类指令
  • Cmpeq和cmpne类指令
  • Mov类指令