【ARMv8 SIMD和浮点指令编程】编程基础

ARM 高级 SIMD 架构、相关的实现和支持软件通常被称为 NEON 技术。AArch32(相当于 ARMv7 的 NEON 指令)和 AArch64 都有 NEON 指令集。两者都可以显著加速在大型数据集上的重复操作。这在媒体编解码器等应用中很有用。AArch64 的 NEON 架构使用 32 × 128 位寄存器,是 ARMv7 的两倍。这些寄存器与浮点指令使用的寄存器相同。

浮点和 NEON 在所有标准 ARMv8 实现中都是必需的。然而,针对特定市场的实现可能支持以下组合:

  • 没有 NEON 和浮点数。
  • 完整的浮点和 SIMD,支持异常捕获。
  • 完整的浮点和 SIMD,不支持异常捕获。

一、AArch64 NEON 和浮点新特性

AArch64 NEON 基于现有的 AArch32 NEON,并进行了以下更改:

  1. 现在有 32 个 128 位寄存器,而 ARMv7 只有 16 个。
  2. 较小的寄存器不再打包到较大的寄存器中,而是一对一地映射到 128 位寄存器的低阶位。单精度浮点数使用低 32 位,而双精度浮点数使用 128 位寄存器的低 64 位。
  3. ARMv7-A 中的 NEON 指令中的 V 前缀已经被删除。
  4. 向向量寄存器写入 64 位或更少的数据会导致更高的位被置零。
  5. 在 AArch64 中,没有在通用寄存器上操作的 SIMD 或饱和算术指令。这种操作使用 NEON 寄存器。
  6. 新的 lane 插入和提取指令已经添加,以支持新的寄存器打包方案。
  7. 额外的指令用于生成或使用 128 位向量寄存器的前 64 位。数据处理指令将结果产生到多个寄存器(扩大到 256 位向量),或消耗两个源(缩小到 128 位向量),这些指令已经被拆分为单独的指令。
  8. 一组新的向量约简操作提供了跨 lane 和、最小和最大值。
  9. 一些现有指令已被扩展以支持 64 位整数值。例如比较、加法、绝对值和负数运算,包括饱和运算。
  10. 饱和指令已经扩展到包括无符号到有符号累加,有符号到无符号累加。
  11. AArch64 NEON 支持双精度浮点和完整的 IEEE 754 操作,包括舍入模式、非规范化数字和 NaN 处理。

浮点数在 AArch64 中得到了增强,并做了如下改动:

  1. ARMv7-A 浮点指令中的 V 前缀已经被 F 替换。
  2. 支持单精度(32 位)和双精度(64 位)浮点向量数据类型和 IEEE 754 浮点标准所定义的运算, 增强 FPCR 取整模式、默认 NaN 控制、Flush-to-Zero 控制和(由具体实现支持的)异常陷阱使能位。
  3. FP/NEON 寄存器的加载/存储寻址模式与整数加载/存储相同,包括加载或存储一对浮点寄存器的能力。
  4. 增加了浮点的 FCSEL 和 Select and Compare 指令,相当于整数的 CSEL 和 CCMP。
  5. 浮点 FCMP、FCMPE、FCCMP 和 FCCMP 根据浮点数比较的结果设置 PSTATE.{N,Z,C,V} 标记,不修改浮点状态寄存器(Floating-Point Status Register,FPSR),就像 ARMv7 中的情况。
  6. 所有浮点乘加和乘减指令都融合在一起。在 VFPv4 中引入了融合乘法,这意味着乘法的结果在被用于加法之前不做四舍五入。在早期的 ARM 浮点架构中,乘加运算在执行中间结果和最终结果都做舍入,这可能会导致少许精度损失。
  7. AArch64 提供了额外的转换操作,例如 64 位整数和浮点数之间的转换,以及半精度和双精度之间的转换。将浮点数转换为整数(FCVTxU、FCVTxS)指令编码为定向舍入模式:
  • 接近 0
  • 趋于 +∞. 朝正无穷
  • 趋于 –∞. 朝负无穷
  • Nearest with ties to even. 就近舍入。若数字位于中间,则偏向舍入到偶数最低有效位。
  • Nearest with ties to away. 就近舍入。偏向远离 0,即四舍五入。
  1. 在浮点格式(FRINTx)中添加了 float 到最近整数的取整,使用相同的定向取整模式,以及根据周围的取整模式进行取整。
  2. 一种新的双精度到单精度的向下转换指令,不精确取整到奇数,适用于带正确取整(FCVTXN )的进行向下转换至半精度的过程。
  3. 增加了 FMINNM 和 FMAXNM 指令,实现了 IEEE754-2008 minNum() 和 maxNum() 操作。如果其中一个操作数是 NaN,则返回数值。
  4. 增加了加速浮点向量归一化的指令(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)变体,长、宽、窄的变体通过后缀表示。
  1. 普通指令可以操作任何向量类型,并产生与操作数向量大小相同、通常类型相同的结果向量。例如:ADD <Vd>.<T>, <Vn>.<T>, <Vm>.<T>

  2. 长(Long)或变长(Lengthening)指令操作双字向量操作数,并产生四字向量结果。结果元素是操作数宽度的两倍。长指令是通过在指令后面加上 L 来指定的。例如:SADDL V0.4S, V1.4H, V2.4H,下图展示了这一点,输入操作数会在操作之前被提升。

在这里插入图片描述

  1. 宽(Wide)或变宽(Widening)指令操作一个双字向量操作数和一个四字向量操作数,产生一个四字向量结果。结果元素和第一个操作数的宽度是第二个操作数元素宽度的两倍。宽指令后面有一个 W。例如:SADDW V0.4S, V1.4H, V2.4S,下图展示了这一点,输入的双字操作数在操作之前被提升。

在这里插入图片描述

  1. 窄(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 位执行操作。
  1. 后缀为 2 的变宽(Widening)指令从包含较窄值的向量的高编号 lane 获取其输入数据,并将扩展结果写入 128 位目标。例如:SADDW2 V0.2D, V1.2D, V2.4S

在这里插入图片描述

  1. 带有 2 后缀的变窄(Narrowing)指令从 128 位源操作数获取其输入数据,并将其缩窄的结果插入 128 位目标的高编号 lane,使较低的 lane 保持不变。例如:XTN2 V0.4S, V1.2D

在这里插入图片描述

  1. 带有 2 后缀的变长(Lengthening)指令从 128 位源向量的高编号 lane 获取其输入数据,并将加长结果写入 128 位目标。例如:SADDL2 V0.2D, V1.4S, V2.4S

在这里插入图片描述

最后再来结合 ZIP1 & ZIP2 指令去理解一下后缀 2。

ZIP1 这条指令从两个源 SIMD&FP 寄存器的下半部分读取相邻的向量元素,并将其成对,然后将这两个向量交叉放置到一个向量中,最后将向量写入目标 SIMD&FP 寄存器。而 ZIP2 是刚好从两个源 SIMD&FP 寄存器的上半部分读取相邻的向量元素,并将其成对。见下图。

在这里插入图片描述

  • 比较指令现在使用条件代码名称来指示条件是什么,以及条件是有符号还是无符号(如果适用),例如 CMGTCMHICMGECMHS

参考资料

  1. 《ARMv8-A-Programmer-Guide》
  2. 《Arm® Architecture Reference Manual for A-profile architecture》

猜你喜欢

转载自blog.csdn.net/tyyj90/article/details/129020265
今日推荐