ARM高级SIMD架构,其相关实现和支持软件通称为NEON技术。AArch32(相当于ARMv7 NEON指令)和AArch64都有NEON指令集。两者都可以显著加速对大型数据集的重复操作。这在媒体编解码器等应用程序中很有用。
AArch64的NEON架构使用了32×128bit的寄存器组(是ARMv7的两倍)。这些寄存器同样可以被浮点指令集使用。所有编译的代码和子程序都符合EABI(其指定了哪些寄存器可以被破坏,哪些寄存器必须保留在一个特定的子程序中)。编译器可以在代码中的任何位置自由使用任何NEON/VFP寄存器来存储浮点值或NEON数据。
浮点和NEON在标准的ARMv8实现中是必须的。然而,针对专业市场的实现允许以下组合:
AArch64中的NEON基于已存在的AArch32的NEON,但是有以下改变:
V
前缀在AArch64中去掉了。浮点支持在AArch64中得到加强,有以下改变:
V
前缀现在被F
替换。FPCR
舍入模式字段,默认NaN控件、清零控制和(在实现支持的情况下)异常陷阱启用位。FP/NEON
寄存器的加载/存储寻址模式与整数加载/存储相同,包括加载或存储一对浮点寄存器的能力。FCSEL
和选择和比较指令,相当于整数 CSEL
和 CCMP
。 浮点 FCMP
、FCMPE
、FCCMP
和 FCCMP
根据浮点比较的结果设置 PSTATE.{N, Z, C, V}
标志,并且不修改浮点状态寄存器中的条件标志( FPSR
),就像 ARMv7 中的情况一样。FCVTxU
、FCVTxS
)指令对定向舍入模式进行编码:
FRINTx
) 将浮点数舍入到最接近的整数,具有相同的定向舍入模式,以及根据环境舍入模式进行舍入。FCVTXN
)。FMINNM
和 FMAXNM
指令,它们实现了 IEEE754-2008 minNum()
和 maxNum()
操作。 如果其中一个操作数是安静的 NaN,则这些返回数值。FRECPX
、FMULX
)。NEON寄存器组的内容是具有相同数据类型的向量组。一个向量被划分为多个通道,且每个通道包含一个数据,称为元素。
一个NEON向量的通道数量取决于向量本身的长度以及向量中数据的元素的大小。
通常,每个NEON指令会使得n
个操作并行执行,其中n
表示输入向量被划分成的通道的数量。两个通道之间不允许出现进位或溢出。向量中元素的排序是从最低有效位开始的。这意味着元素 0 使用寄存器的最低有效位。
NEON和浮点指令集在以下类型的元素上操作:
Note
16位浮点也是支持的,但是仅作为转入或转出的一种形式。16位浮点本身不支持数据处理操作。
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寄存器看作:
D0-D31
。D寄存器叫做双精度寄存器,其存储双精度浮点值。S0-S31
。S寄存器叫做单精度寄存器,其存储单精度浮点值。H0-H31
。H寄存器叫做半精度寄存器,其存储半精度浮点值。Figure 7.3 Floating-point regitsters from the above views
标量数据是指一个单一的数值而不是包含了多个值的向量。一些 NEON 指令使用标量操作数。 寄存器内的标量通过值向量的索引访问。
访问向量的单个元素的一般数组表示法是:
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个标量。
Vn.H[x]
0 ≤ n ≤ 15 0\leq n\leq 15 0≤n≤15范围内。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. |
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位。
Wide指令或 Widening指令对双字向量操作数和四字向量操作数进行运算,产生四字向量结果。 结果元素和第一个操作数是第二个操作数元素宽度的两倍。 宽指令在指令后附加了一个 W
。 例如:
SADDW V0.4S, V1.4H, V2.4S
图7.7显示了这一点,输入双字操作数在输入之前被提升为四字。
Narrow 或 Narrowing 指令对四字向量操作数进行运算,并产生一个双字向量结果。结果元素往往是操作数元素长度的一半。Narrow 指令使用在指令后追加N
来指定。例如:
SUBHN V0.4H, V1.4S, V2.4S
其中,SUBHN
是Subtract returning High Narrow 的意思。
图7.8展示了这一点,输入操作数在输入前被降级。
有符号和无符号 saturating 变体(由 SQ 或 UQ 前缀标识)可用于许多指令,如 SQADD 和 UQADD。 如果结果将超过数据类型的最大值或最小值,saturation 指令将返回该最大值或最小值。 saturation 限制取决于指令的数据类型。
Table 7.2. Saturation ranges
Data 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
添加了一个V
后缀用以跨整个寄存器的所有通道进行操作,例如 ADDV
:
ADDV S0, V1.4S
widening,narrowing 或 lengthening 指令可以在后追加一个2
后缀(上半部分说明符)来表示新的语义。如果2
后缀出现,则新的指令将对带有更窄(narrower)元素的寄存器的高64位进行操作。
2
后缀的Widening指令从包含更窄元素的向量的高位通道里获取输入数据,并将扩展后的结果写入128位的目标向量中。例如: SADDW2 V0.2D, V1.2D, V2.4S
Figure 7.11. SADDW22
后缀的Narrowing指令从128位源向量操作数中获取输入数据,并将得到的narrowed结果插入到128位目标向量的高位通道(低位通道不变)。例如: XTN2 V0.4S, V1.2D
XTN{2}:Extract Narrow.2
后缀的Lengthening指令从128位源向量的高位通道中获取输入数据,并将加长后的结果写入128位目标向量。例如: SADDL2 V0.2D, V1.4S, V2.4S
Figure 7.13. SADDL2comparision指令现在使用条件代码名称来指示条件是什么以及(如果适用)条件是有符号还是无符号,例如 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代码可以用多种方式编写。这里简要列举一些(详见 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 (内部函数),而不是手动汇编: