AArch64浮点与NEON技术
ARM高级SIMD架构,其相关实现和支持软件通称为NEON技术。AArch32(相当于ARMv7 NEON指令)和AArch64都有NEON指令集。两者都可以显著加速对大型数据集的重复操作。这在媒体编解码器等应用程序中很有用。
AArch64的NEON架构使用了32×128bit的寄存器组(是ARMv7的两倍)。这些寄存器同样可以被浮点指令集使用。所有编译的代码和子程序都符合EABI(其指定了哪些寄存器可以被破坏,哪些寄存器必须保留在一个特定的子程序中)。编译器可以在代码中的任何位置自由使用任何NEON/VFP寄存器来存储浮点值或NEON数据。
浮点和NEON在标准的ARMv8实现中是必须的。然而,针对专业市场的实现允许以下组合:
- 没有NEON或者浮点支持
- 带有异常捕获的完整浮点和SIMD支持
- 没有异常捕获的完整浮点和SIMD支持
AArch64中NEON和浮点的新特性
AArch64中的NEON基于已存在的AArch32的NEON,但是有以下改变:
- 相比ARMv7中16个可用寄存器,AArch64现在有32×128bit的寄存器可用。
- 较小的寄存器不再打包到较大的寄存器中,而是一对一映射到128位寄存器的低位。单精度浮点值使用低32位,而双精度浮点值使用128位寄存器的低64位。参考NEON和浮点架构一节
- ARMv7-A Neon指令集中的
V
前缀在AArch64中去掉了。 - 向向量寄存器中写入64位或更少会导致高位为零。
- 在AArch64中,没有对通用寄存器进行操作的SIMD或饱和算术指令。此类操作使用NEON寄存器。
- 添加了新的通道插入和提取指令以支持新的寄存器打包方案
- 提供了用于生成和消费128为寄存器的前64位的附加指令。将生成多个结果寄存器(扩展为256位向量)和消费两个源(缩小为128位向量)的数据处理指令拆分为了独立的指令。
- 一组新的向量归约操作提供跨通道的求和,求最小值和最大值操作。
- 一些现有指令已扩展为支持64位整数值。例如,比较,加法,绝对值和否定,包括饱和版本。
- 饱和指令已扩展包含了无符号累加到有符号,以及有符号累加到无符号。
- AArch64 NEON支持双精度浮点和完整的IEEE754 操作,包含舍入模式、非规范化数字和NaN处理。
浮点支持在AArch64中得到加强,有以下改变:
- 在ARMv7-A中的浮点指令的
V
前缀现在被F
替换。 - 支持IEEE754浮点标准中定义的单精度和双精度浮点向量类型与运算,遵循
FPCR
舍入模式字段,默认NaN控件、清零控制和(在实现支持的情况下)异常陷阱启用位。 FP/NEON
寄存器的加载/存储寻址模式与整数加载/存储相同,包括加载或存储一对浮点寄存器的能力。- 添加了浮点
FCSEL
和选择和比较指令,相当于整数CSEL
和CCMP
。 浮点FCMP
、FCMPE
、FCCMP
和FCCMP
根据浮点比较的结果设置PSTATE.{N, Z, C, V}
标志,并且不修改浮点状态寄存器中的条件标志(FPSR
),就像 ARMv7 中的情况一样。 - 所有浮点乘加和乘减指令进行了融合。 融合乘法是在 VFPv4 中引入的,这意味着乘法的结果在用于加法之前不会四舍五入。 在早期的 ARM 浮点体系结构中,乘法累积运算将对中间结果和最终结果进行舍入,这可能会导致一个小的精度损失。
- 提供了额外的转换操作,例如,在 64 位整数和浮点之间以及半精度和双精度之间。将浮点数转换为整数(
FCVTxU
、FCVTxS
)指令对定向舍入模式进行编码:- 趋于0.
- 趋于 + ∞ +\infty +∞.
- 趋于 − ∞ -\infty −∞
- 只有在整数部分是奇数的时候,小数部分才逢5进1; 偶数时逢5舍去.(nearest with ties to even)
- 逢5必进1 (nearest with ties to away)
- 添加了以浮点格式 (
FRINTx
) 将浮点数舍入到最接近的整数,具有相同的定向舍入模式,以及根据环境舍入模式进行舍入。 - 一种新的双精度到单精度向下转换指令,不精确舍入到奇数,适用于正在进行的向下转换到具有正确舍入的半精度(
FCVTXN
)。 - 添加了
FMINNM
和FMAXNM
指令,它们实现了 IEEE754-2008minNum()
和maxNum()
操作。 如果其中一个操作数是安静的 NaN,则这些返回数值。 - 添加了加速浮点向量归一化的指令(
FRECPX
、FMULX
)。
NEON和浮点架构
NEON寄存器组的内容是具有相同数据类型的向量组。一个向量被划分为多个通道,且每个通道包含一个数据,称为元素。
一个NEON向量的通道数量取决于向量本身的长度以及向量中数据的元素的大小。
通常,每个NEON指令会使得n
个操作并行执行,其中n
表示输入向量被划分成的通道的数量。两个通道之间不允许出现进位或溢出。向量中元素的排序是从最低有效位开始的。这意味着元素 0 使用寄存器的最低有效位。
NEON和浮点指令集在以下类型的元素上操作:
- 32位单精度和64位双精度浮点类型
Note
16位浮点也是支持的,但是仅作为转入或转出的一种形式。16位浮点本身不支持数据处理操作。
- 8位,16位,32位,或64位无符号和有符号整型
- 8位和16位多项式。多项式类型用于代码,例如纠错,它使用有限域的二次幂或 {0,1} 上的简单多项式。 正常的 ARM 整数代码通常使用查找表进行有限域算术。 AArch64 NEON 提供了使用大型查找表的指令。 多项式运算很难从其他运算中合成出来,因此有一个基本的乘法运算非常有用,可以从中合成出其他更大的运算。
NEON 单元可以将寄存器看作两种视图 :
32 × 128 位四字寄存器,V0-V31
,每个都可以如图 7.1 所示:
Figure 7.1 Divisions of the V register
32 个 64 位 D 或双字寄存器,D0-D31
,每个都可以如图 7.2 所示:
Figure 7.2 Divisions of the D register
所有这些寄存器都可以在任何时间访问。软件不用显式切换视图,因为指令的使用决定了合适的视图。
浮点
在AArch64中浮点单元将NEON寄存器看作:
- 32 × 64位D寄存器组
D0-D31
。D寄存器叫做双精度寄存器,其存储双精度浮点值。 - 32 × 32位S寄存器组
S0-S31
。S寄存器叫做单精度寄存器,其存储单精度浮点值。 - 32 × 16位H寄存器组
H0-H31
。H寄存器叫做半精度寄存器,其存储半精度浮点值。 - 以上视图的寄存器组合。
Figure 7.3 Floating-point regitsters from the above views
标量数据和 NEON
标量数据是指一个单一的数值而不是包含了多个值的向量。一些 NEON 指令使用标量操作数。 寄存器内的标量通过值向量的索引访问。
访问向量的单个元素的一般数组表示法是:
<Instruction> Vd.Ts[index1], Vn.Ts[index2]
其中,
Vd
是目标寄存器。
Vn
是第一个源寄存器。
Ts
是元素的大小说明符。
index
是元素的索引。
如下面这个例子:
INS V0.S[1] , V1.S[0]
Figure 7.4. Inserting an element into a vector (INS V0.S[1], V1.S[0])
再如MOV V0.B[3] , W0
指令,寄存器W0
最低有效字节将复制到寄存器V0
的第四个字节处。
Figure 7.5. Moving a scalar to a lane (MOV V0.B[3], W0)
NEON标量可以是8位,16位,32位或64位值。除了乘法指令,访问标量的指令可以访问寄存器中的任何元素。
乘法指令仅允许16位或32位标量,并且只能访问寄存器中前128个标量。
- 16位标量被限制在寄存器
Vn.H[x]
0 ≤ n ≤ 15 0\leq n\leq 15 0≤n≤15范围内。 - 32位标量被限制在寄存器
Vn.S[x]
0 ≤ n ≤ 31 0\leq n \leq 31 0≤n≤31范围内。
浮点参数
使用浮点寄存器将浮点值传递给函数(并从函数返回)。 整数(通用)和浮点寄存器可以同时使用。 这意味着浮点参数在浮点 H、S 或 D 寄存器中传递,而其他参数在整数 X 或 W 寄存器中传递。 AArch64 过程调用标准在任何需要浮点运算的地方都要求使用硬件浮点,因此在 AArch64 状态下没有软件浮点链接。
ARMv8-A 架构参考手册中给出了详细的指令列表,但这里列出了主要的浮点数据处理操作,以显示可以完成的操作:
Table 7.1 | |
---|---|
FABS Sd, Sn | Calculates the absolute value. |
FNEG Sd, Sn | Negates the value. |
FSQRT Sd, Sn | Calculates the square root. |
FADD Sd, Sn, Sm | Adds values. |
FSUB Sd, Sn, Sm | Subtracts values. |
FDIV Sd, Sn, Sm | Divides one value by another. |
FMUL Sd, Sn, Sm | Multiplies two values. |
FNMUL Sd, Sn, Sm | Multiplies and negates. |
FMADD Sd, Sn, Sm, Sa | Multiplies and adds (fused). |
FMSUB Sd, Sn, Sm, Sa | Multiplies, negates and subtracts (fused). |
FNMADD Sd, Sn, Sm, Sa | Multiplies, negates and adds (fused). |
FNMSUB Sd, Sn, Sm, Sa | Multiplies, negates and subtracts (fused). |
FPINTy Sd, Sn | Rounds to an integral in floating-point format (where y is one of a number of rounding mode options) |
FCMP Sn, Sm | Performs a floating-point compare. |
FCCMP Sn, Sm, #uimm4, cond | Performs a floating-point conditional compare. |
FCSEL Sd, Sn, Sm, cond | Floating-point conditional select if (cond) Sd = Sn else Sd = Sm. |
FCVTSty Rn, Sm | Converts a floating-point value to an integer value (ty specifies type of rounding). |
SCVTF Sm, Ro | Converts an integer value to a floating-point value. |
AArch64 NEON指令格式
NEON 和浮点指令的语法进行了许多更改,以与 AArch64 核心整数和标量浮点指令集语法相协调。 指令助记符紧密基于 ARMv7 NEON。
-
ARMv7 NEON指令的
V
前缀在ARMv8中去掉了。一些助记符已重命名,其中删除 V 前缀导致与 ARM 核心指令集助记符发生冲突。 这意味着,例如,现在有同名的指令执行相同的操作,并且可以是 ARM 核心指令、NEON 或浮点,具体取决于指令的语法,例如:ADD W0, W1, W2{, shift #amount}}
和
ADD X0, X1, X2{, shift #amount}}
是A64的基础指令。
ADD D0, D1, D2
是一个标量浮点指令,
ADD V0.4H, V1.4H, V2.4H
是一个NEON向量指令。
-
增加了S,U,F和P前缀,用来表示 有符号(Signed),无符号(Unsigned),浮点(Floating-point),或多项式(Polynomial)数据类型。这个助记符表示操作的数据类型。例如:
PMULL V0.8B, V1.8B, V2.8B
-
向量组织方式(元素大小和通道数)由寄存器限定符描述。例如:
ADD Vd.T, Vn.T, Vm.T
其中,
Vd
Vn
和Vm
是寄存器名称,而T
指示了寄存器如何划分。T
是分配的说明符,可以是8B
,16B
,4H
,8H
,2S
,4S
或2D
其中之一。根据使用64,32,16还是8位数据,以及使用寄存器的64位还是128位,可以使用前述说明符的任意一个。例如,做两个64位通道的加法,使用:ADD V0.2D, V1.2D, V2.2D
-
与 ARMv7 中一样,一些 NEON 数据处理指令可用于 Normal、Long、Wide、Narrow 和 Saturating 变体。 Long、Wide 和 Narrow 变体由后缀表示:
-
Normal 指令可以对任何向量类型进行操作,并产生与操作数向量相同大小且通常类型相同的结果向量。
-
Long 指令或 Lengthening 指令对双字向量操作数进行运算并产生四字向量结果。 结果元素是操作数宽度的两倍。Long 指令使用附加到指令后面的
L
来指定。 例如:SADDL V0.4S, V1.4H, V2.4H
图7.6显示了这一点,输入操作数在输入之前被提升为32位。
Figure 7.6. NEON long instructions
-
Wide指令或 Widening指令对双字向量操作数和四字向量操作数进行运算,产生四字向量结果。 结果元素和第一个操作数是第二个操作数元素宽度的两倍。 宽指令在指令后附加了一个
W
。 例如:SADDW V0.4S, V1.4H, V2.4S
图7.7显示了这一点,输入双字操作数在输入之前被提升为四字。
Figure 7.7. NEON wide instructions
-
Narrow 或 Narrowing 指令对四字向量操作数进行运算,并产生一个双字向量结果。结果元素往往是操作数元素长度的一半。Narrow 指令使用在指令后追加
N
来指定。例如:SUBHN V0.4H, V1.4S, V2.4S
其中,
SUBHN
是Subtract returning High Narrow 的意思。
图7.8展示了这一点,输入操作数在输入前被降级。Figure 7.8. NEON narrow instructions
-
-
有符号和无符号 saturating 变体(由 SQ 或 UQ 前缀标识)可用于许多指令,如 SQADD 和 UQADD。 如果结果将超过数据类型的最大值或最小值,saturation 指令将返回该最大值或最小值。 saturation 限制取决于指令的数据类型。
Table 7.2. Saturation rangesData type Saturation range of x Signed byte (S8) − 2 7 ≤ x < 2 7 -2^7 \leq x < 2^7 −27≤x<27 Signed halfword (S16) − 2 15 ≤ x < 2 15 -2^{15} \leq x < 2^{15} −215≤x<215 Signed word (S32) − 2 31 ≤ x < 2 31 -2^{31} \leq x < 2^{31} −231≤x<231 Signed doubleword (S64) − 2 63 ≤ x < 2 63 -2^{63} \leq x < 2^{63} −263≤x<263 Unsigned byte (U8) 0 ≤ x < 2 8 0 \leq x < 2^8 0≤x<28 Unsigned halfword (U16) 0 ≤ x < 2 16 0 \leq x < 2^{16} 0≤x<216 Unsigned word (U32) 0 ≤ x < 2 32 0 \leq x < 2^{32} 0≤x<232 Unsigned doubleword (U64) 0 ≤ x < 2 64 0 \leq x < 2^{64} 0≤x<264 -
ARMv7中pairwise操作指令的前缀
P
在ARMv8中改为后缀了,例如ADDP
。Pairwise指令对双字或四字操作数的相邻对进行操作。例如:ADDP V0.4S, V1.4S, V2.4S
Figure 7.9. Pairwise operation
-
添加了一个
V
后缀用以跨整个寄存器的所有通道进行操作,例如ADDV
:ADDV S0, V1.4S
Figure 7.10. Across all lanes operation
-
widening,narrowing 或 lengthening 指令可以在后追加一个
2
后缀(上半部分说明符)来表示新的语义。如果2
后缀出现,则新的指令将对带有更窄(narrower)元素的寄存器的高64位进行操作。- 带有
2
后缀的Widening指令从包含更窄元素的向量的高位通道里获取输入数据,并将扩展后的结果写入128位的目标向量中。例如:
Figure 7.11. SADDW2SADDW2 V0.2D, V1.2D, V2.4S
- 带有
2
后缀的Narrowing指令从128位源向量操作数中获取输入数据,并将得到的narrowed结果插入到128位目标向量的高位通道(低位通道不变)。例如:
XTN{2}:Extract Narrow.XTN2 V0.4S, V1.2D
Figure 7.12. XTN2
- 带有
2
后缀的Lengthening指令从128位源向量的高位通道中获取输入数据,并将加长后的结果写入128位目标向量。例如:
Figure 7.13. SADDL2SADDL2 V0.2D, V1.4S, V2.4S
- 带有
-
comparision指令现在使用条件代码名称来指示条件是什么以及(如果适用)条件是有符号还是无符号,例如 CMGT 和 CMHI、CMGE 和 CMHS。
CMGT:Compare signed Greater than
CMHI:Compare unsigned Higher
CMGE:Compare signed Greater than or Equal
CMHS:Compare unsigned Higher or same
NEON 编码的替代方案
NEON代码可以用多种方式编写。这里简要列举一些(详见 ARM NEON Programmer’s Guide 或 笔者翻译的一个简短的NEON入门指南 ),包含使用intrinsics(内在函数,比内嵌汇编更抽象一级的调用方式),C代码的自动向量化,直接使用向量优化的库,当然也包括直接写汇编语句。
Intrinsics(内在函数)是编译器用适当的 NEON 指令替换的 C 或 C++ 伪函数调用。 这允许您使用 NEON 实现中可用的数据类型和操作,同时允许编译器处理指令调度和寄存器分配。 这些内在函数在 ARM C 语言扩展文档中定义。
自动矢量化由 ARM 编译器 6 中的 -fvectorize 选项控制,但在更高的优化级别(-O2 及更高级别)会自动启用。 如果指定 -O0,则即使您指定 -fvectorize,也会禁用自动矢量化。 因此,您将使用以下命令在 -O1 启用自动矢量化:
armclang --target=armv8a-arm-none-eabi -fvectorize -O1 -c file.c
有各种可用的库可以使用 NEON 代码。 此类库的确切状态会随着时间而变化,因此本指南不涵盖当前支持。
尽管在技术上可以手动优化 NEON 汇编代码,但这可能非常困难,因为流水线和内存访问时序具有复杂的相互依赖性。 ARM 强烈建议使用Intrinsic (内部函数),而不是手动汇编:
- 使用 instrinsics 编写代码比使用汇编助记符更容易。
- Instrinsics 为跨平台开发提供了良好的可移植性。
- 无需担心流水线和内存访问时间。
- 在大多数情况下,结果是良好的性能。 如果您不是经验丰富的汇编语言程序员,Instrinsics 通常可以实现比汇编更好的性能。 Instrinsics 提供几乎与编写汇编语言一样多的控制,但将寄存器分配留给编译器,以便您可以专注于算法。 这导致源代码比使用汇编语言更易于维护。