0%

ARM64向量指令基本逻辑整理

为什么要向量指令

完成相同的功能为什么向量指令有时可以提升性能,本质上是编译器(或程序员)提前获得了
需要处理数据的特征,并用特殊的向量指令处理,把本来应该在硬件中处理的逻辑移动到了
编译器这一侧。数据具有一个定的规律是可以使用向量指令处理的前提条件。

比如,我们要计算y[i] = a * x[i] + b,用一般指令就要循环的去计算,在超标量处理器中
这个循环计算可能被并行展开,编译器识别到被处理数据可以并行展开这样的特性,直接使用
向量指令进行计算。

所以,向量指令擅长处理天然具有一定并行度的数据。

ARM64中向量指令

ARMv7中NEON指令是ARM开始对向量指令的支持,ARMv8里增加了SVE,ARMv9里增加了SVE2和SME
的支持。SVE/SME中的S是scalable(可扩展)的意思,通过增加一些谓词寄存器和相应指令提升
了向量指令的可扩展性,所谓可扩展性是说在被处理向量数据发生变化的时候,写好的代码
可以很好的与之适配,比如上面计算y[i] = a * x[i] + b时,不同情况下i的大小可能不一样,
怎么样可以用一份代码支持各种情况。SVE/SVE2中的V是vector(向量)的意思,SME的M是matrix
(矩阵)的意思。

ARM官网上提供了一个简短的SVE指令介绍,具体的位置在这里

基本指令和寄存器

和一般指令一样,需要先把数据从内存放到寄存器上,计算指令的输入和输出都是寄存器,
ARM的向量指令设计中,增加了向量指令使用的寄存器,其中包括Z0-Z31的32个向量寄存器,
P0-P15的16个谓词寄存器,FFR(First Fault Register),以及ZCR_ELx的向量长度寄存器。

增加寄存器就增加了系统状态,与状态相关的上下文保存恢复的各种行为都要考虑,直观看
上去需要考虑的有:1. 过程调用时的寄存器保存恢复,2. 进程切换时的寄存器保存和恢复,
第一点要定义包含向量寄存器的过程调用ABI协议,第二点要操作系统内核支持。

Z0-Z31寄存器的最大长度是2048bit,具体运行时一个寄存器中的元素个数受ZCR_ELx控制,
每个元素的大小是编码到向量指令里的,其中 ZCR_ELx可以是ZCR_EL1/2/3,分别控制低特权
和相同特权级Z0-Z31寄存器中的元素个数,可以看到进程在使用向量寄存器时需要额外的接
口先配置ZCR_EL1x。

我们看一个具体SVE指令ADD(immediate)的编码:

1
2
3
4
5
+-----------------+-------+-----------------+----+--------+-------+
| 31 - 24 | 23 22 | 21 - 14 | 13 | 12 - 5 | 4 - 0 |
+-----------------+-------+-----------------+----+--------+-------+
| 0 0 1 0 0 1 0 1 | size | 1 0 0 0 0 0 1 1 | sh | imm8 | Zdn |
+-----------------+-------+-----------------+----+--------+-------+

其中size编码向量寄存器里每个元素的大小,分别可以是8/16/32/64bit,对应的汇编指令是:

1
2
3
4
ADD Zn.B Zn.B, #imm,shift
ADD Zn.H Zn.H, #imm,shift
ADD Zn.S Zn.S, #imm,shift
ADD Zn.D Zn.D, #imm,shift

如果这时ZCR_EL1为16,那么ADD Z1.B Z1.B, #10就表示把Z1里按8bit为一个元素做划分,
一共有16个元素,把这16个元素每个加上10,然后更新到Z1里。

对于向量的load/store操作,ARM支持所谓gather-load/scatter-store的操作,简单讲就是
可以用向量寄存器表示一组地址,这样就可以实现一个load把一组地址上的数据加载到寄存器,
或者是一个store把数据保存到一组地址上。

这种数据加载和保存方式叫人直接想到的就是发生异常应该如何处理,ARM上使用独立的指令
控制向量load/store的异常行为,一种指令是异常照样触发,另一种是不触发异常,而把异常
发生时成功或失败load/store向量元素的信息保存在FFR寄存器里。

谓词控制

向量指令按照一定的格式做计算,谓词寄存器可以控制计算的格式,提升编程的灵活度。
P0-P7是访存和计算的谓词寄存器,P8-P15是循环控制的谓词寄存器。

比如,加上谓词寄存器的ADD是这样的:

1
2
3
4
5
+-----------------+-------+-------------------+---------+---------------+
| 31 - 24 | 23 22 | 21 - 13 | 12 - 10 | 9 - 5 | 4 - 0 |
+-----------------+-------+-------------------+---------+-------+-------+
| 0 0 0 0 0 1 0 0 | size | 1 0 0 0 0 0 1 1 0 | Pg | Zm | Zdn |
+-----------------+-------+-------------------+---------+-------+-------+

ADD <Zdn>.<T> <Pg>/M, <Zdn>.<T>, <Zm>.<T>根据Pg寄存器中的bit mask对有效位上的数据
做ADD操作,M(merge)表示对于无效位置上的数据不做处理,可见M语意是直接编码到指令里的。

基于谓词的循环控制主要是可以动态的处理向量化时产生的边角数据。

为此ARM增加了谓词生成的指令,比如whilelt/ptrue,ptrue直接根据指令中编码的pattern
在谓词寄存器中产生对应的值,whilelt比较两个输入值,满足比较条件的时候配置谓词寄存
器中的对应bit。

如上谓词生成指令不只根据条件生成谓词数据,而且还会改变PSTATE中的状态寄存器(N/Z/C/V),
配合条件跳转指令,可以控制包含向量指令的循环跳转逻辑。incb之类的指令可以根据pattern
对标量寄存器进行自增运算。这些指令的功能用普通指令也可以完成,但是使用基于向量数据
的指令,指令会更加紧凑。

异常处理

这里看向量指令相关异常的定义,其实关键就是向量的访存指令的异常处理。ARM spec在
D1.3.5里对SVE相关的访存异常做了定义(SVE synchronous memory faults)。一个SVE访存
指令会有三个版本,比如: LD1B、LDFF1B(first fault)、LDNF1B(no fault),其中第一种
指令每次访存都有可能产生异常,第二种只看第一个有效数据的异常,第三种不产生异常。

第一种指令产生异常,并被操作系统修复后,似乎整个指令要全部重复执行?第二种和第三种
指令里被抑制产生的异常信息会被记录到FFR寄存器里。

向量指令用户态使用

可以使用各种向量指令库,编译器对于一些场景也可以自动编译出向量指令,手写向量汇编
也是一种选择。

相关内核支持

异常处理

(todo)

上下文保存恢复

Linux内核采用lazy的向量寄存器上下文保存办法,代码路径在:arch/arm64/kernel/fpsimd.c。

性能

向量指令主要用户是HPC等特定场景,可以看到Apple M系列处理器(M1-3)里都不支持向量指令,
Intel的桌面处理器上支持的AVX指令被Linus认为“不务正业”。

似乎向量指令的性能需要从整体功能的角度衡量,还不清楚怎么从微架构性能数据(perf)看
向量指令的性能?