ARM 高级 SIMD 架构、相关的实现和支持软件通常被称为 NEON 技术。AArch32(相当于 ARMv7 的 NEON 指令)和 AArch64 都有 NEON 指令集。两者都可以显著加速在大型数据集上的重复操作。这在媒体编解码器等应用中很有用。AArch64 的 NEON 架构使用 32 × 128 位寄存器,是 ARMv7 的两倍。这些寄存器与浮点指令使用的寄存器相同。
浮点和 NEON 在所有标准 ARMv8 实现中都是必需的。然而,针对特定市场的实现可能支持以下组合:
- 没有 NEON 和浮点数。
- 完整的浮点和 SIMD,支持异常捕获。
- 完整的浮点和 SIMD,不支持异常捕获。
一、AArch64 NEON 和浮点新特性
AArch64 NEON 基于现有的 AArch32 NEON,并进行了以下更改:
- 现在有 32 个 128 位寄存器,而 ARMv7 只有 16 个。
- 较小的寄存器不再打包到较大的寄存器中,而是一对一地映射到 128 位寄存器的低阶位。单精度浮点数使用低 32 位,而双精度浮点数使用 128 位寄存器的低 64 位。
- ARMv7-A 中的 NEON 指令中的 V 前缀已经被删除。
- 向向量寄存器写入 64 位或更少的数据会导致更高的位被置零。
- 在 AArch64 中,没有在通用寄存器上操作的 SIMD 或饱和算术指令。这种操作使用 NEON 寄存器。
- 新的 lane 插入和提取指令已经添加,以支持新的寄存器打包方案。
- 额外的指令用于生成或使用 128 位向量寄存器的前 64 位。数据处理指令将结果产生到多个寄存器(扩大到 256 位向量),或消耗两个源(缩小到 128 位向量),这些指令已经被拆分为单独的指令。
- 一组新的向量约简操作提供了跨 lane 和、最小和最大值。
- 一些现有指令已被扩展以支持 64 位整数值。例如比较、加法、绝对值和负数运算,包括饱和运算。
- 饱和指令已经扩展到包括无符号到有符号累加,有符号到无符号累加。
- AArch64 NEON 支持双精度浮点和完整的 IEEE 754 操作,包括舍入模式、非规范化数字和 NaN 处理。
浮点数在 AArch64 中得到了增强,并做了如下改动:
- ARMv7-A 浮点指令中的 V 前缀已经被 F 替换。
- 支持单精度(32 位)和双精度(64 位)浮点向量数据类型和 IEEE 754 浮点标准所定义的运算, 增强 FPCR 取整模式、默认 NaN 控制、Flush-to-Zero 控制和(由具体实现支持的)异常陷阱使能位。
- FP/NEON 寄存器的加载/存储寻址模式与整数加载/存储相同,包括加载或存储一对浮点寄存器的能力。
- 增加了浮点的 FCSEL 和 Select and Compare 指令,相当于整数的 CSEL 和 CCMP。
- 浮点 FCMP、FCMPE、FCCMP 和 FCCMP 根据浮点数比较的结果设置 PSTATE.{N,Z,C,V} 标记,不修改浮点状态寄存器(Floating-Point Status Register,FPSR),就像 ARMv7 中的情况。
- 所有浮点乘加和乘减指令都融合在一起。在 VFPv4 中引入了融合乘法,这意味着乘法的结果在被用于加法之前不做四舍五入。在早期的 ARM 浮点架构中,乘加运算在执行中间结果和最终结果都做舍入,这可能会导致少许精度损失。
- AArch64 提供了额外的转换操作,例如 64 位整数和浮点数之间的转换,以及半精度和双精度之间的转换。将浮点数转换为整数(FCVTxU、FCVTxS)指令编码为定向舍入模式:
- 接近 0
- 趋于 +∞. 朝正无穷
- 趋于 –∞. 朝负无穷
- Nearest with ties to even. 就近舍入。若数字位于中间,则偏向舍入到偶数最低有效位。
- Nearest with ties to away. 就近舍入。偏向远离 0,即四舍五入。
- 在浮点格式(FRINTx)中添加了 float 到最近整数的取整,使用相同的定向取整模式,以及根据周围的取整模式进行取整。
- 一种新的双精度到单精度的向下转换指令,不精确取整到奇数,适用于带正确取整(FCVTXN )的进行向下转换至半精度的过程。
- 增加了 FMINNM 和 FMAXNM 指令,实现了 IEEE754-2008 minNum() 和 maxNum() 操作。如果其中一个操作数是 NaN,则返回数值。
- 增加了加速浮点向量归一化的指令(FRECPX、FMULX)。
二、NEON 和浮点架构
NEON 寄存器的内容是由相同数据类型的元素组成的向量。向量被划分为 lane,每个 lane 包含一个称为元素的数据值。NEON 向量的 lane 数取决于向量的大小和其中的数据元素。通常,每个 NEON 指令会导致 n 个操作并行发生,其中 n 是输入向量被划分的 lane 数。不能从一个 lane 进位或溢出到另一个 lane。向量中元素的顺序是从最低有效位开始的。这意味着元素 0 使用寄存器的最低有效位。
NEON 指令和浮点指令操作以下类型的元素:
- 32 位单精度浮点和 64 位双精度浮点。支持 16 位浮点数,但它仅作为转换自/到的格式,不支持数据处理操作。
- 8 位、16 位、32 位或 64 位无符号和有符号整数。
- 8 位和 16 位多项式。
NEON 单元将寄存器文件显示为:32 × 128 位四字寄存器,V0~V31,每个寄存器如下图所示:
32 个 64 位 D 或双字寄存器 D0~D31, 每个寄存器如下图所示:
所有这些寄存器都可以在任何时候访问。软件不需要在它们之间显式切换,因为使用的指令决定了适当的视图。
2.1 浮点
在 AArch64 中,浮点单元将 NEON 寄存器文件视为:
- 32 × 64 位D寄存器 D0~D31。D 寄存器称为双精度寄存器,包含双精度浮点值。
- 32 × 32 位S寄存器 S0~S31。S 寄存器称为单精度寄存器,包含单精度浮点值。
- 32 × 16 位H寄存器 H0~H31。H 寄存器称为半精度寄存器,包含半精度浮点值。
- 来自上述视图的寄存器的组合。
2.2 标量数据和 NEON
标量数据是指单个值,而不是包含多个值的向量。有些 NEON 指令使用标量操作数。寄存器内的标量可以通过值向量的索引访问。
访问向量中单个元素的一般数组表示法如下:
<Instruction> Vd.Ts[index1], Vn.Ts[index2]
其中:
Vd
是目标寄存器。
Vn
是第一个源寄存器。
Ts
是元素的大小说明符。
Index
是元素的索引。
例如:
向向量中插入一个元素:INS V0.S[1], V1.S[0]
将标量移动到 lane:MOV V0.B[3], W0
(将寄存器 W0 的最低字节复制到寄存器 V0 的第四个字节)
NEON 标量可以是 8 位、16 位、32 位或 64 位值。除了乘法指令,访问标量的指令可以访问寄存器文件中的任何元素。
乘法指令只允许 16 位或 32 位标量,并且只能访问寄存器文件中的前 128 个标量:
-
16 位标量仅限于 Vn.H[x] 寄存器,0≤n≤15。
-
32 位标量仅限于 Vn.S[x] 寄存器。
2.3 浮点参数
浮点值使用浮点寄存器传递给函数(或从函数返回)。整数(通用)寄存器和浮点寄存器都可以同时使用。这意味着浮点参数用浮点 H、S 或 D 寄存器传递,其他参数用整数 X 或 W 寄存器传递。AArch64 过程调用标准强制要求在任何需要浮点运算的地方使用硬件浮点,因此在 AArch64 状态下不存在软件浮点。
这里列出了主要的浮点数据处理操作,以展示可以完成的操作:
浮点指令 | 指令含义 |
---|---|
FABS Sd, Sn | 计算绝对值。 |
FNEG Sd, Sn | 对值取反。 |
FSQRT Sd, Sn | 计算平方根。 |
FADD Sd, Sn, Sm | 相加。 |
FSUB Sd, Sn, Sm | 相减。 |
FDIV Sd, Sn, Sm | 相除。 |
FMUL Sd, Sn, Sm | 相乘。 |
FNMUL Sd, Sn, Sm | 相乘后取反。 |
FMADD Sd, Sn, Sm, Sa | 乘后再加。 |
FMSUB Sd, Sn, Sm, Sa | 乘后再减。 |
FNMADD Sd, Sn, Sm, Sa | 乘,取反后再加。 |
FNMSUB Sd, Sn, Sm, Sa | 乘,取反后再减。 |
FPINTy Sd, Sn | 浮点格式取整(y 是许多舍入模式选项之一)。 |
FCMP Sn, Sm | 浮点比较。 |
FCCMP Sn, Sm, #uimm4, cond | 浮点条件比较。 |
FCSEL Sd, Sn, Sm, cond | 浮点条件选择 if (cond) Sd = Sn else Sd = Sm。 |
FCVTSty Rn, Sm | 浮点值转换为整数值(ty 指定舍入类型)。 |
SCVTF Sm, Ro | 整数值转换为浮点值。 |
比如 FADD <Vd>.<T>, <Vn>.<T>, <Vm>.<T>
,浮点加法(向量)。该指令将两个源 SIMD&FP 寄存器中对应的向量元素相加,将结果写入向量,再将向量写入目标 SIMD&FP 寄存器。这条指令中的所有值都是浮点值。该指令可以产生浮点异常,根据 FPCR 中的设置,异常会导致在 FPSR 中设置标志或生成同步异常。
是一个排列说明符,对于半精度,取值为 4H 或者 8H;对于单精度和双精度,取值为 2S、4S 和 2D。
SIMD&FP 目标寄存器
SIMD&FP 第一源寄存器
SIMD&FP 第二源寄存器
三、AArch64 NEON 指令格式
为了与 AArch64 核心的整数和标量浮点指令集的语法保持一致,NEON 和浮点指令集的语法做了一些修改。指令助记符接近基于 ARMv7 NEON。
- ARMv7 的 NEON 指令的
V
前缀已被删除。
一些助记符已经被重命名,其中删除 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 向量指令。
比如加法指令的详细解读:
加(向量)。这条指令在两个源 SIMD&FP 寄存器中相加相应的元素,将结果放入一个向量,并将该向量写入目标 SIMD&FP 寄存器。
标量 ADD <V><d>, <V><n>, <V><m>
向量 ADD <Vd>.<T>, <Vn>.<T>, <Vm>.<T>
宽度说明符,编码在“size”字段中。它的取值:D(当 size = 11 时),以下编码是保留的:
• size = 0x
• size = 10
是 SIMD&FP 目标寄存器的编号,位于“Rd”字段中。
是第一个 SIMD&FP 源寄存器的编号,编码在“Rn”字段中。
是第二个 SIMD&FP 源寄存器的编号,编码在“Rm”字段中。
是 SIMD&FP 目标寄存器的名称,编码在“Rd”字段中。
是一个排列说明符,编码在“size:Q”字段中。它可以有以下值:
• 8B:当 size = 00,Q = 0 时
• 16B:当 size = 00,Q = 1 时
• 4H:当 size = 01,Q = 0 时
• 8H:当 size = 01,Q = 1 时
• 2S:当 size = 10,Q = 0 时
• 4S:当 size= 10,Q = 1 时
• 2D:当 size = 11,Q = 1 时
• 编码 size = 11,Q = 0 是保留的
是第一个 SIMD&FP 源寄存器的名称,编码在“Rn”字段中。
是第二个 SIMD&FP 源寄存器的名称,编码在“Rm”字段中。
- 添加了 S、U、F 或 P 前缀来表示有符号、无符号、浮点或多项式数据类型。这个助记符表示操作的数据类型。
例如:PMULL V0.8B, V1.8B, V2.8B
(PMULL 将两个源 SIMD&FP 寄存器的向量的下半部分或上半部分中的相应元素相乘,将结果放入一个向量中,并将向量写入目标 SIMD/FP 寄存器。目标向量元素的长度是相乘的元素的两倍)
- 向量组织结构(元素大小和 lane 数)由寄存器限定符描述。
例如: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 位寄存器。要添加 2 × 64 位的 lane ,则使用 ADD V0.2D, V1.2D, V2.2D
。
- 与 ARMv7 一样,有些 NEON 数据处理指令有普通的(Normal)、长的(Long)、宽的(Wide)、窄的(Narrow)和饱和的(Saturating)变体,长、宽、窄的变体通过后缀表示。
-
普通指令可以操作任何向量类型,并产生与操作数向量大小相同、通常类型相同的结果向量。例如:
ADD <Vd>.<T>, <Vn>.<T>, <Vm>.<T>
-
长(Long)或变长(Lengthening)指令操作双字向量操作数,并产生四字向量结果。结果元素是操作数宽度的两倍。长指令是通过在指令后面加上 L 来指定的。例如:
SADDL V0.4S, V1.4H, V2.4H
,下图展示了这一点,输入操作数会在操作之前被提升。
- 宽(Wide)或变宽(Widening)指令操作一个双字向量操作数和一个四字向量操作数,产生一个四字向量结果。结果元素和第一个操作数的宽度是第二个操作数元素宽度的两倍。宽指令后面有一个 W。例如:
SADDW V0.4S, V1.4H, V2.4S
,下图展示了这一点,输入的双字操作数在操作之前被提升。
- 窄(Narrow)或变窄(Narrowing)指令操作四字向量操作数,并产生双字向量结果。结果元素通常是操作数元素宽度的一半。窄指令使用在指令后面附加一个 N 来指定。例如:
SUBHN V0.4H, V1.4S, V2.4S
,下图展示了这一点,在操作之前,输入操作数会被收窄。
- 有符号和无符号饱和变体(由 SQ 或 UQ 前缀标识)可用于许多指令,如 SQADD 和 UQADD。如果结果超过数据类型的最大值或最小值,饱和指令会返回该最大值或最小值。饱和限制取决于指令的数据类型。饱和范围见下表。
数据类型 | x 的饱和范围 |
---|---|
Signed byte (S8) | -2^7 <= x < 2^7 |
Signed halfword (S16) | -2^15 <= x < 2^15 |
Signed word (S32) | -2^31 <= x < 2^31 |
Signed doubleword (S64) | -2^63 <= x < 2^63 |
Unsigned byte (U8) | 0 <= x < 2^8 |
Unsigned halfword (U16) | 0 <= x < 2^16 |
Unsigned word (U32) | 0 <= x < 2^32 |
Unsigned doubleword (U64) | 0 <= x < 2^64 |
- 用于成对操作的 ARMv7 P 前缀现在是 ARMv8 中的后缀,例如在 ADDP 中。成对指令作用于相邻的双字或四字操作数对。例如:
ADDP V0.4S, V1.4S, V2.4S
。
- 后缀 V 用于跨 lane (全寄存器)操作,例如 ADDV。
ADDV S0, V1.4S
。
- 后缀 2,称为第二和上半说明符,已被添加为新的变宽、变窄或变长第二部分指令。如果存在,它将导致对保持较窄元素的寄存器的高 64 位执行操作。
- 后缀为 2 的变宽(Widening)指令从包含较窄值的向量的高编号 lane 获取其输入数据,并将扩展结果写入 128 位目标。例如:
SADDW2 V0.2D, V1.2D, V2.4S
- 带有 2 后缀的变窄(Narrowing)指令从 128 位源操作数获取其输入数据,并将其缩窄的结果插入 128 位目标的高编号 lane,使较低的 lane 保持不变。例如:
XTN2 V0.4S, V1.2D
- 带有 2 后缀的变长(Lengthening)指令从 128 位源向量的高编号 lane 获取其输入数据,并将加长结果写入 128 位目标。例如:
SADDL2 V0.2D, V1.4S, V2.4S
最后再来结合 ZIP1
& ZIP2
指令去理解一下后缀 2。
ZIP1 这条指令从两个源 SIMD&FP 寄存器的下半部分读取相邻的向量元素,并将其成对,然后将这两个向量交叉放置到一个向量中,最后将向量写入目标 SIMD&FP 寄存器。而 ZIP2 是刚好从两个源 SIMD&FP 寄存器的上半部分读取相邻的向量元素,并将其成对。见下图。
- 比较指令现在使用条件代码名称来指示条件是什么,以及条件是有符号还是无符号(如果适用),例如
CMGT
和CMHI
、CMGE
和CMHS
。
参考资料
- 《ARMv8-A-Programmer-Guide》
- 《Arm® Architecture Reference Manual for A-profile architecture》