7 AArch64浮点数和NEON
我们把与ARM系列高级处理器SIMD架构相关实现和其对软件的支持通常被称为NEON技术。实际上对于AArch32(相当于 ARMv7 NEON 指令)和AArch64均有NEON指令集。两者均可用于处理大量数据中重复操作的情形,且具有显著的效果,因此可以用在多媒体领域。
AArch64的NEON 架构使用 32 × 128 位寄存器,是 ARMv7 的两倍。这些寄存器与浮点指令使用的寄存器相同。所有编译过的代码和子例程都符合EABI,它指定在特定的子例程中哪些寄存器的内容可以修改,哪些寄存器必须保留。
所有标准 ARMv8 实现都需要浮点数和 NEON。然而,针对特定市场,目前浮点数,SMID和NEON有以下几种组合:
• 没有NEON 或浮点。
• 支持完整的异常捕获浮点数 和SIMD。
• 支持完整的浮点数和SIMD,无异常捕获
7.1 AArch64中NEON和浮点数的新功能
AArch64 NEON在现有AArch32 NEON的基础上进行了以下改动:
>现在有32个128位寄存器,而在ARMv7中只具有16个
>较小的寄存器不再被打包到较大的寄存器中,而是一对一地映射到128位寄存器的低位。单精度浮点数使用低32位,而双精度浮点数使用128位寄存器的低64位。详细内容请参考本章第二小节的内容 .
>ARMv7-A NEON指令中的前缀V被移除。
>向向量寄存器写入64位或更少位的值会导致高位被清零。
>在AArch64中,无法采用通用寄存器上执行SIMD或饱和算术指令,该类操作需要使用NEON寄存器。
>增加了新的通道插入和提取指令,用来支持新的寄存器打包方案。
>提供了用于生成或使用128位向量寄存器的高64位的附加指令。数据处理指令已经被分割成单独的指令,他会生成一个以上的结果寄存器(扩大到256位向量),或消耗两个源(缩小到一个128位向量),。
>一组新的向量规约操作提供了跨通道和(across-lane sum)、最小值和最大值
>一些现有的指令已经扩展到支持64位整数值。如比较、加法、绝对值和取反指令,包括饱和版本。
>饱和指令进行了扩展,把无符号累加包含到了有符号中,将有符号累加包含到了无符号累加中
>AArch64 NEON支持双精度浮点和全精度浮点操作,包括舍入模式、非规范化数字和NaN处理。
通过以下更改,AArch64中的浮点功能得到了增强:
>将ARMv7-A浮点指令中的前缀V替换为F
>支持IEEE 754浮点标准定义的单精度(32位)和双精度(64位)浮点向量数据类型和算法,支持FPCR舍入模式字段(FPCR Rounding Mode field)、默认NaN控件、刷新到零控件(Flush-to-Zero)和异常陷阱启用位(在实现方案支持的情况下)
>FP/NEON寄存器的加载/存储寻址模式与整数加载/存储相同,包括加载或存储一对浮点寄存器的能力
>支持浮点操作中的条件选择和比较指令,其等效于整数操作中的条件选择指令CSEL和比较指令CCMP。
浮点指令FCMP、FCMPE、FCCMP和FCCMP会根据浮点指令的结果来设置标志PSTATE.{N,Z,C,V},但不会修改浮点状态寄存器(FPSR)中的条件标志,如同ARMv7中那样。
>所有浮点乘加(Multiply-Add)和乘减(Multiply-Subtrac)指令都是融合的。
融合乘法是在VFPv4中引入的,这意味着乘法的结果在用于加法之前不会四舍五入。在早期的ARM浮点体系架构中,乘法累加操作将会对执行中间结果和最终结果的四舍五入操作,这种操作可能会导致损失精度的。
>提供了额外的转换操作,例如,64位整数和浮点之间以及半精度和双精度之间的转换操作。
将浮点转换为整数的指令(FCVTxU、FCVTxS)将直接采有向舍入的方式进行编码:
—趋近于0
—趋近于正无穷
—趋近于负无穷
—与偶数关系最近
—距离最近
>添加了具有把浮点数四舍五入到最近整数的指令FRINTx
>一种新的双精度到单精度的向下转换指令,其不精确取整到奇数,可以通过FCVTXN来进行向下转化到半精度。
>添加了FMINNM和FMAXNM指令,这两个指令用来实现IEEE754-2008 中的操作minNum()和maxNum()。如果其中一个操作数是静态NaN,则返回数值
>增加了加速浮点向量规范化的指令(FRECPX、FMULX)
7.2 NEON和浮点架构
NEON寄存器的内容是具有相同数据类型元素的向量。向量被划分为通道,每个通道包含一个称为元素的数据值。
NEON向量中的通道数取决于向量的大小及其向量中的数据元素。
通常,每个NEON指令有n个并行操作,其中n就是输入向量被划分的通道数。不能从一个通道进位或者溢出到另一个通道的。向量中元素的顺序是从最低有效位开始的。这意味着元素0使用寄存器的低有效位。
NEON和浮点指令可以对以下类型的元素进行操作:
>32位单精度和64位双精度浮点数。
注意:16位浮点数也是支持的,但它仅作为转换自/到的数据格式,不支持数据处理操作。
>8位、16位、32位或64位无符号和有符号整数。
>8位和16位多项式。
多项式类型用于代码,比如进行错误纠正,使用的是有限域的2的幂或者{0,1}上的简单多项式,普通ARM整数代码通常使用查找表来进行有限域运算。AArch64 NEON提供了使用大型查找表的指令。
多项式运算很难从其他运算中合成出来,因此有一个基本的乘法运算是非常有必要的,我们可以从这个乘法运算合成其他更复杂的运算。
NEON将寄存器看作:
32个128位的四字寄存器V0-V31,如下图7-1所示:
图 1 V类寄存器划分
32个64位D或双字寄存器D0-D31,每个寄存器如图7-2所示:
图 2 D类寄存器划分
所有这些寄存器都可以在任何时候访问。软件不需要显式地在它们之间切换,因为使用的指令决定了适当的视图。
7.2.1 浮点
在AArch64中,浮点单元将NEON寄存器看作:
>32个64位寄存器D0-D31。D寄存器称为双精度寄存器,其寄存器中数据是双精度浮点数。
>32个32位寄存器S0-S31。S寄存器称为单精度寄存器,其寄存器中数据是单精度浮点数。
>32 个16位寄存器H0-H31。H寄存器称为半精度寄存器,其寄存器中数据是半精度浮点数。
>上述3种寄存器的组合。
图 3 浮点寄存器划分
7.2.2 标量数据和NEON
标量数据指的是单个值,而不是包含多个值的向量。有些NEON指令使用标量操作数。寄存器内的标量是通过向量的索引值来访问。
访问向量中单个元素的通用的数组表示法是:
其中:
Vd是目标寄存器。
Vn是第一个源寄存器。
Ts是元素的大小说明符。
Index是元素的索引。
如下面的例子:
INS V0.S[1], V1.S[0]
图 4插入一个元素到向量中INS V0.S[1], V1.S[0]
在指令MOV V0.B[3], W0中,将寄存器W0的最低字节复制到寄存器V0的第四个字节。
图 5 移动一个标量到通道中MOV V0.B[3], W0
NEON标量可以是8位、16位、32位和64位。除了乘法指令,访问标量的指令可以访问寄存器中的任何元素。
乘指令只允许16位或32位标量,并且只能访问寄存器中的前128位的标量:
>16位标量被限制在寄存器Vn.H[x]中,其中 0≤n≤15。
>32位标量被限制在寄存器Vn.S[x]中。
7.2.3 浮点参数
浮点参数是通过浮点寄存器传递给函数(或从函数返回)。整数寄存器(通用寄存器)和浮点寄存器都可以同时使用。这意味着浮点参数在浮点H、S或D寄存器中传递,而其他参数在整数X或W寄存器中传递。根据AArch64调用标准,调用时会强制在需要浮点运算的地方采用浮点硬件单元,因此在AArch64状态下没有软件浮点链接方式。
在ARMv8-A架构参考手册中给出了详细的指令列表,在此处仅列出了主要的浮点数据处理操作,用来说明其可以完成的功能:
FABS Sd, Sn | 计算绝对值 |
---|---|
FNEG Sd, Sn | 取反 |
FNEG 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 | 将整数转化为浮点数 |
7.3 AArch64 NEON指令格式
为了同AArch64的整数和标量浮点指令集的语法一致,NEON和浮点指令的语法做过不少改动。其指令助记符基于ARMv7。
> 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向量指令
>添加了S、U、F或P前缀来表示有符号、无符号、浮点或多项式(仅其中一种)数据类型。该助记符指出了操作的数据类型。例如: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位的寄存器。
要添加2 × 64位通道,可以使用ADD V0.2D, V1.2D, V2.2D
>在ARMv7中,一些NEON数据处理指令存在Normal, Long,wide,narrow和饱和版本。其Long, Wide 和 Narrow的版本用后缀表示:
普通指令可以对任何向量类型进行操作,并生成与操作数向量大小相同、类型通常相同的结果向量。
长指令或加长指令对双字向量操作数进行操作,并生成四字向量结果。结果元素的宽度是操作数宽度的两倍。长指令是在指令后面附加L来指定的。例如:SADDL V0.4S, V1.4H, V2.4H
图7-6给了长指令的演示,操作数在操作之前被提升。
图 6 NEON长指令
宽指令或加宽指令(Wide or Widening)对双字向量操作数和四字向量操作数进行操作,生成的是四字向量结果。结果元素和第一个操作数的宽度是第二个操作数元素宽度的两倍。宽指令是在指令后面附件W来表示的。例如:SADDW V0.4S, V1.4H, V2.4S
图7-7给了宽指令的演示,输入的双字操作数在操作之前被提升。
图 7 NENO宽指令
窄化或窄化(Narrow or Narrowing)指令对四字向量操作数进行操作,并生成双字向量结果。结果元素通常是操作数元素宽度的一半。窄指令是在指令后面附件N来表示的。例如:SUBHN V0.4H, V1.4S, V2.4S。
图7-8给了窄指令的演示,输入的双字操作数在操作之前被降级。
图 8 NEON窄指令
>有符号和无符号饱和变量(由前缀SQ或UQ标识)可用于许多指令,如SQADD和UQADD。如果结果超过数据类型的最大值或最小值,饱和指令将返回该最大值或最小值。饱和限制取决于指令的数据类型。
数据类型 | 数据范围 |
---|---|
Signed byte (S8) | -27 <= x <27 |
Signed halfword (S16) | -215<= x < 215 |
Signed word (S32) | -231<= x < 231 |
Signed doubleword (S64) | -263<= x < 263 |
Unsigned byte (U8) | 0<= x < 28 |
Unsigned halfword (U16) | 0<= x < 216 |
Unsigned word (U32) | 0<= x < 232 |
Unsigned doubleword (U64) | 0<= x < 264 |
>在ARMv7中成对操作的前缀是P,而在ARMv8中,P是后缀,例如在ADDP中,成对指令对相邻的双字或四字操作数对进行操作。例如: ADDP V0.4S, V1.4S, V2.4S
图 9 对操作
>添加后缀V用于跨通道(整个寄存器)操作,例如ADDV S0, V1.4S
图 10 跨通道操作
>后缀2,称为第二和上半说明符,添加这个后缀用来扩大、缩小或加长第二部分指令。如果后缀2存在,它会导致在存放较窄元素寄存器的高64位上执行操作:
后缀为2的加宽指令从包含较窄值的向量高编号通道获取输入数据,并将扩展后的结果写入128位的目的地址。例如: SADDW2 V0.2D, V1.2D, V2.4S
图 11 SADDW2
后缀为2的窄指令从128位源操作数获取其输入数据,并将其窄化结果插入128位的目的地,并保持低通道不变。例如: XTN2 V0.4S, V1.2D
图 12 XTN2
后缀为2的长指令从128位源向量的高编号通道获取输入数据,并将加长后的结果写入128位的目的地。例如: SADDL2 V0.2D, V1.4S, V2.4S
图 13 SADDL2
>比较指令使用条件代码名来指示条件是什么以及条件是有符号的还是无符号的(如果适用),例如,CMGT和CMHI、CMGE和CMHS。
7.4 NEON编程的选择
NEON的代码有许多种写法。此处我简要列出了几种方法(您可以参阅 ARM NEON Programmers Guide 获取更多细节)。这包括使用内部函数(intrinsic), C代码的自动向量化,库的使用,采用汇编语言等方法。
内部函数是C或C++伪函数调用,编译器将替换为对应的NEON指令。因此我们可以采用NEON中可用的数据类型和操作,同时允许编译器处理指令调度和寄存器分配。这些原语在ARM 扩展语言文档中都有定义。
自动矢量化由ARM编译器6中的-fvectorize选项进行控制,但当采用更高的优化级别(-O2及以上)时会自动启用。即使指定了-fvectorize,自动矢量化也会在级别-O0时禁用。因此,我们可以采用下面的命令在优化级别是-O1时启用自动矢量化:
armclang –target=armv8a-arm-none-eabi -fvectorize -O1 -c file.c
有各种库可以使用NEON代码。但这些库会随着时间的推移而变化,因此本文档中不包括当前这方面的内容。
尽管从技术角度考虑,NEON汇编是可以进行手动优化的,但因为管道和内存访问时序是具有复杂的相互依赖性,因此NEON优化是相当困难的。ARM强烈建议使用内部函数(intrinsic),而不是直接使用汇编:
使用instrinsic编写代码会比使用汇编助记符更容易
instrinsic为跨平台开发提供了良好的可移植性
无需担心管道和内存访问
通常情况下,其性能较好
如果您不是一个擅长使用汇编编写代码的程序员,那同汇编相比,采用instrinsic通常能获得更好的性能。Instrinsic提供了几乎与汇编语言一样多的功能,且编译器会复杂寄存器的分配,这样您就可以专注于代码算法部分,同汇编语言相比,这将会更有利于您对源代码的维护。