翻译可能有偏差,描述可能有错误,请以原著为准
Chapter 1 : Introduction
Chapter 2 : Compiling NEON Instructions
本章介绍了NEON指令集语法
NEON指令语法简介
NEON指令(以及VFP指令)均以字母V开头。指令能够对不同的数据类型进行操作,类型在指令编码中指定,大小以指令的后缀表示,元素的数量由指定的寄存器大小指示。例如下面的指令:
VADD.I8 D0, D1, D2
VADD
表示一个NEON的加操作。I8
后缀表示相加的数据为8位的整形数。D0
,D1
,D2
表示用的是64位寄存器(D0
用来存结果,D1
和D2
用来存操作数)。 下一条指令中,输入和输出寄存器的大小是不一样的:
VMULL.S16 Q2, D8, D9
这条指令将4个16位有符号数据相乘,产生4个有符号的32位数据,存到Q2
里面。
VCVT
指令在单精度浮点与32位整数、定点与半精度浮点(如果有实现)之间转换元素。
NEON指令集包括用于将单个或多个值加载或存储到寄存器的指令。此外,还有一些指令可以在多个寄存器和存储器之间传输数据块。在这样的多次传输期间也可以对数据进行交错(interleave)或解交错(de-interleave)。
NEON指令集包括:
NEON指令集不包括:
VRECPE
和VRECPS
代替,执行牛顿迭代法(Newton-Raphson iteration))。VRSQRTE
和VRSQRTS
和乘法代替)。由于NEON指令执行多项操作,因此它们不能将标准ALU(算数逻辑运算单元)标志用在比较指令上。相反,可以比较两个元素,并将比较结果存储在目标寄存器中。如果测试条件为假,则目标寄存器中每个通道的所有位都设置为0,如果测试条件为真,则设置为1。然后可以对该位使用掩码,来控制随后的指令要操作的数据。有按位选择指令可以与这些位掩码一起使用。
一些NEON指令与向量一起作用于标量。与向量一样,标量的大小可以为8位,16位,32位或64位。使用标量的指令可以访问寄存器文件中的任何元素,尽管与乘法指令有所不同。 该指令使用双字向量的索引来指定标量值。乘法指令仅支持16位或32位标量,并且只能访问寄存器文件中的前32个标量(即对于16位标量为D0
-D7
,对于32位标量则为D0
-D15
)。
有许多不同的指令可在寄存器之间或元素之间移动数据。指令也可以交换或复制寄存器,执行逆运算,矩阵转置并提取单个矢量元素。
指令语法
指令集的通用格式如下:
V{<mod>}<op>{<shape>}{<cond>}{.<dt>} <dest1>{, <dest2>}, <src1>{, <src2>}
是修饰符(前置修饰符):
Q
(Saturating)H
(Havling)D
(Doubling)R
(Rounding)
是要执行的操作,如ADD
、SUB
、MUL
等。
是修饰符(后置修饰符):
L
(Long)W
(Wide)N
(Narrow)
条件,与IT指令一起使用。
<.dt>
数据类型。
源寄存器。有一些指令含有立即数。
目的寄存器。目的寄存器可能是好几个寄存器。
指令修饰符
对于某些指令,您可以指定一个修饰符,该修饰符会更改操作的行为。
修饰符 | 行为 | 例子 | 描述 |
---|---|---|---|
无 | 基本操作 | VADD.I16 Q0, Q1, Q2 |
这样的结果是没有任何修改的 |
Q | Saturation | VQADD.I16 D0, D2, D3 |
如果结果向量中的每个元素超出可表示范围,则将其设置为一个最大值或最小值。范围取决于元素的类型(位数和符号)。如果在任何通道(lane)中发生饱和(saturation),则FPSCR中的粘性QC位置1。 |
H | Halved | VHADD.S16 Q0, Q1, Q4 |
每个元素右移一位(实际上是被截断除以二)。VHADD 可用于计算两个输入的平均值。 |
D | Doubled before saturation | VQDMULL.S16 Q0, D1, D3 |
在以Q15格式乘以数字时,通常需要这样做,在这种情况下,需要额外加倍以使结果转换为正确的格式。 |
R | Rounded | VRSUBHN.I16 D0, Q1, Q3 |
该指令对结果进行舍入,以校正由截断引起的偏差。这等效于在截断之前将结果加0.5。 |
指令shape
结果向量和操作数向量具有相同数量的元素。但是,结果中元素的数据类型可能不同于一个或多个操作数中元素的数据类型。因此,结果的寄存器大小也可能与一个或多个操作数的寄存器大小不同。寄存器大小中的这种关系由形状描述。对于某些说明,您可以指定shape
修饰符(后缀)。
VADD.I16 Q0, Q1, Q2
VADDL.S16 Q0, D2, D3
(D寄存器是64位,Q寄存器是128位。)VADDHN.I16 D0, Q1, Q2
VADDW.I16 Q0, Q1, D4
详解:
Q
,来指定正常指令的操作数和结果必须全部为四字(四字,quadword,一般128位)。这样做的话,如果操作数或结果不是全部为四字,汇编器就会报错。L
来表示长指令。下面图1显示了一个长指令示例,其中输入操作数在操作之前被提升为四字。W
表示宽指令。下面图2显示了这一点,在操作之前将输入的双字操作数提升为四字。指定数据类型
有的NEON指令比其他指令需要知道更多的数据类型。
数据移动 / 替换指令:
标准加法:
饱和(saturating)指令:
操作数的数据类型在指令中指定。通常,只有第二个操作数的数据类型必须被指定。
大多数指令的允许数据类型范围有限。 但是,数据类型的描述是很灵活的:
I
,你同样可以使用S
或U
的数据类型。I
,S
,U
,P
或F
)。 F16
数据类型仅在实现半精度体系结构扩展的系统上可用,并且仅适用于转换。
打包和解包数据
对于NEON指令,多个数据元素被打包到单个寄存器中,以允许单个指令同时对寄存器中的所有数据元素进行操作。
打包数据很常见,但有时无法通过NEON指令对打包的数据进行一些处理。因此,必须将数据解压缩到寄存器中。
一种方法是加载打包的数据,通过清除不需要的位来掩盖所需的部分,然后将结果左移或右移。这种方法是可行的,但是效率相对较低。
NEON打包和解包指令简化了打包数据和解压缩数据之间的转换。这使得可以使用字或双字加载有效地加载存储器中的打包数据序列,将其解压缩到单独的寄存器值中,对其进行操作,然后打包回寄存器中,以有效地写出到存储器中。见图:
对齐
NEON体系结构为NEON数据访问提供了完全的未对齐支持。但是,指令操作码包含了对齐指示,当地址是对齐的,并且指定了对齐指示的时候,能使对齐更快。然而,如果指定了对齐方式但地址未正确对齐,则会发生数据中止。
基地址的指定方式为:[
注意:指定了对齐提示,然后使用不正确的对齐地址是编程上的错误。
对齐指示可以是:64
、:128
、:256
位其中的一种,这要根据D寄存器的数量而定。
VLD1.8 {D0}, [R1:64]
VLD1.8 {D0,D1}, [R4:128]!
VLD1.8 {D0,D1,D2,D3}, [R7:256], R2
注意:ARM体系指导手册用@
标志来描述,但在源代码里面不推荐用这个。
GNU的gas编译器仍将接受[Rn,:128]
语法(注意冒号之前额外的“,”),但还是首选[Rn:128]
语法。
这适用于Cortex-A8和Cortex-A9处理器。对于Cortex-A8处理器,指定128位或更高的对齐方式可节省一个周期。对于Cortex-A9处理器,指定64位或更高的对齐方式可节省一个周期。
饱和算法
上一节的指令修饰符中,用Q
修饰符来指示饱和运算。这是一种算术形式,其中将数学运算的结果限制为预定的最大值和最小值。如果函数的结果大于最大饱和值,则将其设置为最大值。如果小于最小饱和值,则将其设置为最小值。
例如,如果饱和值的范围为[-10,10]
,则以下操作的结果如下:
饱和算术常用于数字信号处理之类的应用中,避免像音频信号超出最大范围之类的情况。
浮点操作
NEON的浮点不是完全符合IEEE-754的。
非规格数(denormals)被刷新为零(flush to zero)。
Rounding固定取最近的数(其实就是四舍五入),但转换操作除外。
仅支持单精度算术(.F32
)。
单独的(标量)浮点指令。
浮点异常
在可能导致浮点异常的指令描述符中,有一块subsection列出了异常。如果指令说明中没有“浮点异常”的subsection,则该指令不会导致任何浮点异常。
刷新到0模式
“刷新到0”(flush-to-zero)模式将非规格(denormalized)数替换为0。这不符合IEEE-754算术,但是在某些情况下,它可以大大提高性能。
在NEON和VFPv3中,“刷新到0”会保留符号位。
在VFPv2中,被刷为 + 0 +0 +0。(不管符号,都刷为正?)
NEON单元经常用“刷新到0”模式。
异常
NEON单元符合IEEE 754-1985标准,但仅支持舍入到最近的舍入(rounding)模式。这是大多数高级语言(例如C和Java)使用的舍入模式。此外,NEON单元始终将非规格值(denormals)视为零。
在浮点算术中,非规格(denormal)是指浮点数有效位的前面是0
。 这意味着数值非常小,尾数的格式为 0. m 1 m 2 m 3 m 4 . . . m p − 1 m p 0.m_1m_2m_3m_4...m_{p-1}m_p 0.m1m2m3m4...mp−1mp,指数是最小可能指数。 m m m是0或1的有效数字,p是精度。
这里的一些概念和术语请参考IEEE745标准,关于浮点数的规范
“刷新到0”模式的作用
除某些例外,“刷新到0”模式对浮点运算有以下影响:
每当非规格(denormalized)数被当做操作数,或者结果被刷新到0的时候,会发生一个inexact
异常。在“刷新到0”模式下不会发生Underflow
异常。
不受“刷新到0”模式影响的操作
即使在“刷新到0”模式下,也可以对非规格化(denormalized)的数字执行以下NEON操作,不会把结果刷新为零:
VMOV
,VMVN
,V {Q} ABS
和V {Q} NEG
)VDUP
)VSWP
)VLDR
和VSTR
)VLDM
和VSTM
)VMOV
)位移操作
本节介绍NEON移位操作,并说明如何使用它们在常用的色深之间转换图像数据。
NEON指令集提供的强大的移位指令范围支持:
位移向量
NEON移位操作与标量ARM代码中的移位非常相似。移位将向量的每个元素(element)中的位向左或向右移动。每个元素左侧或右侧超出的位将被丢弃,它们不会转移到相邻元素。
移位量可以用指令中的立即数,或附加的移位向量指定。当使用移位向量时,输入向量的每个元素的移位取决于移位向量中相应元素的值。位移向量中的元素被视为有符号值,因此在每个元素的基础上可以向左,向右和零移位。
对有符号元素的向量进行右移操作(由指令所附的类型指示),将对每个元素进行符号扩展。这等效于ARM代码中的算术转换。应用于无符号向量的移位不对扩展符号进行符号化。
位移和插入
带插入操作的NEON移位提供了一种组合两个向量中的位的方法。例如,向左移动并插入(VSLI
)将源向量的每个元素向左移动。每个元素右侧插入的新位是目标向量中的相应位。如下图所示:
位移和累加
有NEON指令可将向量的元素右移,并将结果累加到另一个向量中。这对于在将结果与较低精度的累加器组合之前,以较高的精度进行临时计算的情况很有用。和图6的操作差不多,把“插入”换成了“相加”。
指令修饰符
每个移位指令可以带有一个或多个修饰符。这些修改器本身不会更改移位操作,但是会调整输入或输出以消除偏置(bias)或饱和(saturate)到某个范围。移位指令有五个修饰符:
R
表示N
表示L
表示Q
表示Q
前缀和U
后缀表示。这类似于饱和(saturating)修饰符,但是当给定有符号或无符号输入时,结果饱和到无符号范围。某些修饰符的组合是没啥用的,因此没有针对它们的NEON指令。例如,饱和右移(称为VQSHR)就没什么用,因为右移会使结果更小,因此该值不能超出可用范围。
可用位移表
下表列出了所有NEON移位指令,它们是按照前面提到的修饰符排列的。如果仍然不确定修饰语字母的含义,请使用表3,表4和表5选择所需的说明。在表中,Imm
是偏移量的立即数,Reg
是位移向量的寄存器。
Not rounding | rounding | |||||
---|---|---|---|---|---|---|
Default | Long | Narrow | Default | Long | Narrow | |
Left(Imm) | VSHL | VSHLL | ||||
Left(Reg) | VSHL | VRSHL | ||||
Right(Imm) | VSHR | VSHRN | VRSHR | VRSHRN | ||
Left insert(Imm) | VSLI | |||||
Right insert(Imm) | VSRI | |||||
Right accumulate(Imm) | VSRA | VRSRA |
Not rounding | rounding | |||||
---|---|---|---|---|---|---|
Default | Long | Narrow | Default | Long | Narrow | |
Left(Imm) | VQSHL | |||||
Left(Reg) | VQSHL | VQRSHL | ||||
Right(Imm) | VQSHRN | VQRSHRN |
Not rounding | rounding | |||||
---|---|---|---|---|---|---|
Default | Long | Narrow | Default | Long | Narrow | |
Left(Imm) | VQSHLU | |||||
Left(Reg) | ||||||
Right(Imm) | VQSHRUN | VQRSHRUN |
多项式
多项式是由任何变量的幂和构成的表达式,其中每个被加数都有一个系数。关于变量 x x x的一个多项式例子: a 2 x 2 + a 1 x + a 0 a_2x^2+a_1x+a_0 a2x2+a1x+a0。
如果数据类型位于 { 0 , 1 } \{0,1\} {0,1}上,则可以使用数据类型P8(8位多项式)和P16(16位多项式)来表示多项式。 { 0 , 1 } \{0,1\} {0,1}上的多项式是系数为0或1的多项式。对于系数不是0或1的多项式,不能使用NEON多项式算法。
如果 f f f是一个这样的多项式,使用P8数据类型中的8位, f f f可以表示系数为 { a 7 , a 6 , a 5 , a 4 , a 3 , a 2 , a 1 , a 0 } \{a_7,a_6,a_5,a_4,a_3,a_2,a_1,a_0\} {a7,a6,a5,a4,a3,a2,a1,a0}的序列,其中 a n a_n an为0或1。或者,您可以使用P16数据类型将 f f f表示为序列 { a 15 , a 14 , a 13 , a 12 , a 11 , a 10 , a 9 , a 8 , a 7 , a 6 , a 5 , a 4 , a 3 , a 2 , a 1 , a 0 } \{a_{15},a_{14},a_{13},a_{12},a_{11},a_{10},a_9,a_8,a_7,a_6,a_5,a_4,a_3,a_2,a_1,a_0\} {a15,a14,a13,a12,a11,a10,a9,a8,a7,a6,a5,a4,a3,a2,a1,a0}。
因此,NEON寄存器D或Q 中的每个8位或16位通道(lane)都包含多项式的系数序列,例如 f f f。
注意: 您可以将1s和0s的多项式序列可视化为8位或16位无符号值。但是,多项式算法与常规算法不同,并且会产生不同的结果。
{0,1}上的多项式算术
多项式算法在实现某些密码学或数据完整性算法时很有用。
多项式系数0和1使用布尔算术规则进行操作:
对 { 0 , 1 } \{0,1\} {0,1}上的两个多项式相加,相当于与按位异或(OR)。因此,多项式相加会导致与常规相加不同的值。
对 { 0 , 1 } \{0,1\} {0,1}上的两个多项式相乘,首先确定按常规乘法完成的部分乘积,然后对部分乘积进行异或运算,而不是按常规方式进行相加。多项式乘法的结果与常规乘法不同,因为它需要部分乘积的多项式加法。
多项式类型可以帮助那些必须使用2的幂的有限域或简单多项式的任何事物。普通的ARM整数代码通常用查表的方法来进行有限域的算术运行,但大型的查找表无法向量化。多项式运算很难从其他运算中合成出来,因此具有基本的乘法运算(加法运算为按位异或)是有用的,从中可以合成较大的乘法或其他运算。
多项式算法包括:
可以进行多项式计算的NEON指令
有一些NEON指令可对多项式数据类型P8和P16进行操作。乘法指令是唯一的指令,能够改变多项式数据类型的行为,把它从常规的乘法转为多项式乘法。当使用多项式数据类型时,VMUL
和VMULL
是仅有的两个可以执行多项式乘法的指令。
VADD
指令执行常规加法,不能用于执行多项式加法。多项式加法与按位异或运算完全相同,因此对于多项式加法,必须使用VEOR
指令。
但是,VEOR
内部函数(instrinsic)不接受多项式数据类型P8和P16。因此,在使用内部函数(instrinsic)时,必须将数据类型从P8或P16重新解释为VEOR
内部函数(instrinsic)接受的数据类型之一。 为此,请使用NEON内部函数(instrinsic)vreinterpret
。
多项式乘法与常规乘法之间的区别
多项式加法表现为按位异或(无进位加法),而不是常规加法(带有进位的加法)。即使在单个位上的多项式乘法与在单个位上的常规乘法相同,这也会导致多项式乘法的差异。部分结果必须是按位异或运算,而不是常规地相加。在某些系统中,这称为无进位乘法。
下面的例子显示了常规算术中3乘3的乘积。乘法以二进制显示,以突出显示差异。 乘法的结果是b01001
,也就是十进制的9,这是常规乘法所期望的。 不会用latex打竖式公式,直接截图了:
下一个例子显示多项式算术中3与3的乘积。乘法以二进制显示,以突出显示差异。当对位值进行运算时,多项式乘法与常规乘法相同,对中间过程得到的乘积的多项式加法是异或。多项式相乘的结果为b00101
,等于5。
置换向量的指令
当可用的算术指令与寄存器中的数据格式不匹配时,在向量处理中有时需要进行置换(permutation)或更改向量中元素的顺序。他们从一个寄存器或跨多个寄存器中选择单个元素,以形成一个新向量,该向量与处理器提供的NEON指令更好地匹配。
置换(permutation)指令与移动(move)指令相似,因为它们用于准备或重新排列数据,而不是修改数据的值。好的算法设计可以无需重新排列数据。因此,请考虑在您的代码中是否需要置换指令。降低对置换和移动指令的需求通常是一种更好的方式。
备用方案
下面有一些方法可以避免不必要的置换(permutation):
如果你考虑了上述所有方法,还是不能得到合适的数据格式,那就不得不用用置换指令了。
指令
本节内容请参考我的另外一篇文章 ,有配图。
置换指令有很多,从简单的翻转到随意地重构向量。简单的置换(permutation)可以在一个指令周期内搞定,而更复杂的操作则需要多个周期,并且可能需要设置其他寄存器。 与往常一样,请查看处理器的《技术参考手册》以获取性能详细信息。
VMOV
和VSWP
是最简单的置换(permute)指令,可以将整个寄存器的内容复制到另一个寄存器中,或者交换一对寄存器中的值。
尽管您可能不会将它们视为置换指令,但是可以利用两个D寄存器来交换一个Q寄存器的高低位。 例如,VSWP d0,d1
交换q0
的最高和最低有效64位。
VREV
反转向量中8位,16位或32位元素的顺序,有三种变体:
VREV16
反转构成向量的16位元素的每对8位子元素。
VREV32
反转构成向量的32位元素的每对16位子元素,或4个8位子元素。
VREV64
反转构成向量的64位元素的每对32位子元素,或4个16位子元素,或8个8位子元素。
VREV
反转数据的字节序,通常用于重新排列颜色分量或交换音频样本的通道。
VEXT
从一对现有向量中提取字节组成新的向量。新向量中的字节从第一个操作数的顶部到第二个操作数的底部。这使您可以生成一个新向量,其中包含跨越一对现有向量的元素。
VEXT
可以用来在两个向量上实现滑动窗口,在FIR滤波器上非常有用。对于置换(permutation),当两个输入操作数使用相同的向量时,也可以用来模拟按字节旋转的操作。
VTRN
在一对向量之间转置8、16或32位元素。 它将向量的元素视为2x2矩阵,并转置每个矩阵。
使用多个VTRN
指令来转置较大的矩阵。例如,由16位元素组成的4x4矩阵可以使用三条VTRN
指令进行转置。
在加载向量之后或存储向量之前,这与VLD4
和VST4
执行的操作相同。 由于它们需要较少的指令,因此请尽可能尝试使用这些结构化的内存访问功能,而不要使用一系列VTRN
指令。
VZIP
交错(interleaves)一对向量的8、16或32位元素。在存储数据之前,该操作与VST2
执行的操作相同。所以如果需要在写回内存之前立即压缩数据,请使用VST2
,而非VZIP
。
VUZP
是VZIP
的反向操作,反交错(deinterleaving)一对向量的8位,16位或32位元素。在从内存加载数据之后,该操作与VLD2
所做操作相同。
VTBL
从向量表和索引向量构造一个新的向量。这是一个逐字节的表查找操作。
该表由1到4个相邻的D寄存器组成。 索引向量中的每个字节都用于索引向量表中的一个字节。 将索引值插入到结果向量中与索引向量中原始索引位置相对应的位置。
VTBL
和VTBX
在处理超出范围索引的方式上有所不同。如果一个索引超过了表的长度,VTBL
会在结果向量的相应位置插入0,但是VTBX
保持结果向量中的值不变(如上图,d2
原来是多少就是多少)。
如果将单个源向量用作表,则VTBL
允许您实现向量的任意排列,但要以设置索引寄存器为代价。 如果是在一个循环里面使用该操作,并且排列的类型不变,则可以在循环的外部初始化索引寄存器,并消除设置开销。