翻译可能有偏差,描述可能有错误,请以原著为准
Chapter 1 : Introduction
Chapter 2 : Compiling NEON Instructions
Chapter 3 : NEON Instruction Set Architecture
本章介绍了ARM编译器工具链如何提供内部函数(intrinsics),为所有处于ARM和Thumb状态的Cortex-A系列处理器生成NEON代码。
简介
内部函数(intrinsics)由ARM编译器工具链提供,用intrinsics来写NEON代码,比写汇编程序更容易维护,同时仍然可以控制生成哪些NEON指令。
NEON的intrinsics像一种函数调用,编译器会用适当的NEON指令或NEON指令序列来替换它。但是你必须优化你的代码,以充分利用NEON带来速度的提高。
这里有一些数据类型来对应NEON的寄存器(包括D寄存器和Q寄存器),包括不同size的元素。然后你可以创建一些C语言的变量直接映射到NEON寄存器,这些变量会传到NEON intrinsics函数里。编译器会直接生成NEON指令,而不是执行一个函数调用。
intrinsics的函数和数据类型,以及简短形式的intrinsics,为C / C++代码访问低层的NEON功能提供了支持。软件可以传递NEON向量,就像处理函数参数或者返回值那样。也可以像一般变量那样声明NEON向量。
intrinsics提供了绝大多数控制,和写汇编差不多,但是把寄存器的分配留给编译器来做,这样你就可以专注于你的算法了。另外,编译器会像普通C / C++代码那样优化intrinsics,有可能的话,会把你写的代码替换成更高效的序列。
NEON的intrinsics都在arm_neon.h
头文件里面定义了,里面也包含了向量类型。
注意:
在ARMv7架构之前是不支持NEON指令集的。在早期的架构上面编译,或者在不包含NEON单元的ARMv7(有的处理器虽然是ARMv7架构,但是不带NEON)架构上编译,编译器会把NEON intrinsics当成普通的函数调用,这样会导致错误。
简介
NEON向量数据类型根据以下模式命名:
例如:
int16x4_t
是一个向量,其中包含4个16位的短整型变量(4个lane,每个lane存16位的数)。float32x4_t
是一个向量,其中包含4个32位的浮点型变量(4个lane,每个lane存32位的数)。下表列出了向量数据类型:
64位类型(D寄存器) | 128位类型(Q寄存器) |
---|---|
int8x8_t |
int8x16_t |
int16x4_t |
int16x8_t |
int32x2_t |
int32x4_t |
int64x1_t |
int64x2_t |
uint8x8_t |
uint8x16_t |
uint16x4_t |
uint16x8_t |
uint32x2_t |
uint32x4_t |
uint64x1_t |
uint64x2_t |
float16x4_t |
float16x8_t |
float32x2_t |
float32x4_t |
poly8x8_t |
poly8x16_t |
poly16x4_t |
poly16x8_t |
你可以用上述向量类型来指定intrinsics的输入和输出。一些intrinsics可以用向量类型的数组,它们组合了2个、3个或者4个相同的向量类型:
这些类型是原始的C结构体,包含一个叫做val
的元素。
这些类型映射到<被NEON加载和存储时所访问的>寄存器上,以实现用一条指令加载 / 访问多达4个寄存器。
该结构体的一个定义示例如下:
struct int16x4x2_t
{
int16x4_t val[2];
} <var_name>;
这些类型仅在加载、存储、转置(transpose)、交错(interleave)、反交错(de-interleave)指令中使用,为了对实际数据执行操作,从单个寄存器中选取元素,例如
和
。
有的数组类型长度为2~4,其中包含的向量类型都列在表1
了。
注意:
向量数据类型和向量数据类型的数组不能通过直接文本赋值初始化。您可以使用一个‘加载’ intrinsics 或使用vcreate
intrinsics来初始化它们。
NEON intrinsics 的原型
intrinsics使用与NEON统一汇编语法类似的命名规则:
它提供了一个额外的标志q
来指示intrinsics操作128位的向量。
例如:
vmul_s16
将两个有符号16位向量的值相乘。VMUL.I16 d2, d0, d1
vaddl_u8
是一个长指令,将两个包含无符号8位值的64位向量相加。结果是128位的向量,内含无符号16位的值。VADDL.U8 q1, d0, d1
除了指令中写到的寄存器,别的寄存器也可能会被用到,这要看编译器怎么优化。它甚至会以某些方式得到不同的编译结果,以便提高性能。
注意:
那些用到__fp16
的intrinsics,仅在拥有NEON半精度VFP扩展的平台上可用。
要开启__fp16
的话,要在命令行输入--fp16_format
选项,详情见ARM编译器工具链的文档。
使用NEON intrinsics
ARM编译工具链定义了intrinsics,详情在arm_neon.h
头文件里,表4.1
中的向量类型也包含在内。
intrinsics是ARM ABI的一部分,因此可以在ARM编译工具链和GCC之间移植。
带q
后缀的intrinsics通常操作于Q寄存器, 不带q
后缀的intrinsics通常操作于D寄存器, 然而有一些还是用的Q寄存器。
下面的例子展示了一个intrinsics,带有两个不同变量:
uint8x8_t vadd_u8(uint8x8_t a, uint8x8_t b);
这个vadd_u8
不含q
后缀,在此例中,输入输出向量都是64位向量,用的是D寄存器。
uint8x16_t vaddq_u8(uint8x16_t a, uint8x16_t b);
上面这个vaddq_u8
带有q
后缀,因此输入和输出向量都是128位,用了Q寄存器。
uint16x8_t vaddl_u8(uint8x8_t a, uint8x8_t b);
上面这个vaddl_u8
不含q
后缀,输入向量是64位,输出向量是128位。
一些NEON intrinsics使用32位ARM通用寄存器作为输入参数来保存标量值。例如有的intrinsics从一个向量提取单个值(vget_lane_u8
)、设置向量的某个通道(vset_lane_u8
)、从一个字面的值创建一个向量(vcreate_u8
)、把向量的所有通道设置到某个值(vdup_n_u8
)。
对每种类型使用不同的intrinsics,会导致很难在不兼容的类型之间进行突发性操作,因为编译器会对“哪些寄存器持有哪些类型”保持追踪。
编译器同样会重新安排程序流,使用可选的更快的指令集。不能保证生成的指令集一定与intrinsic所指示的指令集相匹配。这在从一个微体系结构转移到另一个微体系结构的时候特别有用。
例1
的代码展示了一个函数,用一个4通道(每个通道是32位无符号整形)的向量作为输入参数,返回一个值(所有通道都被加倍了)。
#include
uint32x4_t double_elements(uint32x4_t input)
{
return(vaddq_u32(input, input));
}
例2
是从例1
生成的反汇编代码,该代码(指例1
)是为hard float ABI编译的。double_elements()
函数转换成一条NEON指令和一个返回序列。
double_elements PROC
VADD.I32 q0,q0,q0
BX lr
ENDP
例3
也是例1
的反汇编版本,该代码(指例1
)是为software linkage编译的。在这种情况下,在使用前必须把参数从ARM通用寄存器拷贝到一个NEON寄存器。完成计算后,再把返回值从NEON寄存器拷贝到ARM通用寄存器。
double_elements PROC
VMOV d0, r0, r1
VMOV d1, r2, r3
VADD.I32 q0, q0, q0
VMOV r0, r1, d0
VMOV r2, r3, d1
BX lr
ENDP
GCC和armcc支持相同的intrinsic,因此用NEON intrinsic编写的代码在工具链之间是完全可移植的。必须在每个使用intrinsic的源文件中包含arm_neon.h
头文件,并且必须指定命令行选项。
使用intrinsic优化源模块是很有用的,就算处理器没有实现NEON技术,也可以为它编译。__ARM_NEON__
宏是GCC定义的,在为实现可NEON技术的目标编译的时候定义。RVCT 4.0 build 591或更高版本,以及ARM编译器工具链也定义了这个宏。软件可以使用这个宏来提供文件中的<优化过的或者纯C/C++版本的>函数,这些函数由传递给编译器的命令行参数选择。
对于intrinsic函数和向量数据类型的更多信息,请查看《ARM Compiler toolchain Compiler Reference Guide》,GCC文档。
NEON代码里的变量和内容
本节是一些例子,用NEON代码访问变量数据和常量数据。
声明变量
声明一个新的变量,和C代码里面的一样简单:
uint32x2_t vec64a, vec64b; // create two D-register variables
使用常量
使用常量很简单,下面的代码把一个常量复制到一个向量的每个元素里:
uint8x8 start_value = vdup_n_u8(0);
要将一般64位常量加载到向量中,请使用:
uint8x8 start_value = vreinterpret_u8_u64(vcreate_u64(0x123456789ABCDEFULL));
把结果返回到普通的C变量
若要从NEON寄存器访问结果,请用VST
把它保存到内存中,或者用 “get lane” 类型的操作把它移回ARM寄存器。
result = vget_lane_u32(vec64a, 0); // extract lane 0
从Q寄存器中访问D寄存器
用vget_low
和vget_high
从D寄存器访问Q寄存器。
vec64a = vget_low_u32(vec128); // split 128-bit vector
vec64b = vget_high_u32(vec128); // into 2x 64-bit vectors
在不同的NEON变量类型之间转换
NEON的intrinsic是强类型的,在不同的向量类型之间必须显式转换。在D寄存器上用vreinterpret
来转换,在Q寄存器上用vreinterpretq
。这些intrinsic不生成任何代码,只是让你可以转换NEON类型。
uint8x8_t byteval;
uint32x2_t wordval;
byteval = vreinterpret_u8_u32(wordval);
uint8x16_t byteval2;
uint32x4_t wordval2;
byteval2 = vreinterpretq_u8_u32(wordval2);
注意:
输出类型u8是在vreinterpret
后面列出来的,在输出类型u32之前。
从C访问向量类型
必须包含arm_neon.h
头文件,才能用intrinsic和C风格的类型进行向量操作。C类型以下面的形式编写:
uint8x16_t
:这是一个向量,包含无符号8位整形。向量中有16个元素,因此该向量必须用128位Q寄存器int16x4_t
:这是一个向量,包含有符号16位整形,向量中有4个元素,因此向量必须在64位的D寄存器中。由于ARM标量类型和NEON向量类型之间是不兼容的,就算它们之间具有相同的位长,也无法把标量分配到向量中。标量值和指针只能和直接使用标量的NEON指令一起使用。
例如,要从NEON向量的通道0取出无符号的32位整数,用:
result = vget_lane_u32(vec64a, 0)
在armcc中,除了赋值以外,不能用标准C运算符来操作向量类型。因此用VADD
intrinsic 更为合适,而不是用操作符+
。但是,GCC允许标准C运算符对NEON向量类型进行操作,从而使代码更具可读性。
如果向量类型只是元素的数量不同(uint32x2_t, uint32x4_t)
,有专门的指令将128位值的上半部分或者下半部分的向量元素分配给64位寄存器,反之亦然。如果可以将寄存器安排为别名(scheduled as aliases),则此操作不使用任何代码空间。要用128位寄存器的低64位,如下:
vec64 = vget_low_u32(vec128);
从内存加载到向量
本节介绍如何使用NEON intrinsic 创建向量。内存中的连续数据可以加载到单个向量或多个向量, NEON intrinsic 干这件事的是vld1_datatype
。 例如,要加载一个向量(4个16位无符号数据),请使用NEON intrinsic vld1_u16
。
下面的例子中,数组A包含8个16位元素,此例展示了如何把数据从这个数组加载到一个向量中。
#include
#include
unsigned short int A[ ] = {1,2,3,4}; // array with 4 elements
int main(void)
{
uint16x4_t v; // declare a vector of four 16-bit lanes
v = vld1_u16(A); // load the array from memory into a vector
v = vadd_u16(v,v); // double each element in the vector
vst1_u16(A, v); // store the vector back to memory
return 0;
}
你可以查看intrinsic的说明(在文档附录),看看vldl_datatype
可以用哪些datatype
。
从字面位模板创建向量
你可以从字面值来创建向量,NEON的intrinsic用vcreate_datatype
来做这件事。例如你想要加载一个向量(8个无符号8位数据),你可以用intrinsicvcreate_u8
。下面例子展示了如何从字面数据创建一个向量。
#include
int main (void)
{
uint8x8_t v; // define v as a vector with 8 lanes of 8-bit data
unsigned char A[8]; // allocate memory for eight 8-bit data
v = vcreate_u8(0x0102030405060708); // create a vector that contains the values 1,2,3,4,5,6,7,8
vst1_u8(A, v); // store the vector to memory, in this case, to array A
return 0;
}
更多关于vcreate_datatype
支持的类型请看文档附录。
从交错的内存中构建多个向量
在内存中的数据经常是交错(interleaved)的,NEONintrinsic支持2路,3路,4路交错模式。
例如内存的一块区域中可能包含立体声数据,左右通道的数据是交错的,这是一个2路交错模式的例子。
还有一个例子:内存中包含24位RGB图像,它是从红、绿、蓝通道3路交错的8位数据。当内存中包含交错数据的时候,“解交错(de-interleaving)”可以让你加载一个向量存储所有红色值,一个单独的向量存储所有绿色值,一个单独的向量存储所有蓝色值。
NEON 解交错的 intrinsic 是vldn_datatype
,其中n
表示交错模式,可以是2
,3
,4
,如果你想把24位RGB图像解交错到3个不同的向量中,可以用vld3_u8
。下面看看怎么做:
#include
int main (void)
{
uint8x8x3_t v; // This represents 3 vectors.
// Each vector has eight lanes of 8-bit data.
unsigned char A[24]; // This array represents a 24-bit RGB image.
v = vld3_u8(A); // This de-interleaves the 24-bit image from array A
// and stores them in 3 separate vectors
// v.val[0] is the first vector in V. It is for the red channel
// v.val[1] is the second vector in V. It is for the green channel
// v.val[2] is the third vector in V. It is for the blue channel.
v.val[0] = vadd_u8(v.val[0],v.val[0]); // Double the red channel
vst3_u8(A, v); // store the vector back into the array, with the red channel doubled.
return 0;
}
更多关于vldn_datatype
intrinsic的类型看文档附录的Load and store部分。
从内存中加载单个通道的向量
如果要从分散在内存中的数据构造向量,则必须使用单独的 intrinsic 分别加载每个通道。
实现这个的NEON intrinsic 是vld1_lane_datatype
。例如你要加载向量的一个通道(8位无符号数据),要用vld1_lane_u8
。下面的例子展示了怎样从内存中加载向量的单个通道:
#include
#include
????(文档里面没东西)
编程
直接在汇编器中或通过 intrinsic 函数接口编写NEON代码,需要对使用的数据类型以及可用的NEON指令有透彻的了解。要知道要使用什么NEON运算,查看如何将算法拆分为并行运算很有用。从SIMD的角度来看,交换操作(例如加,最小和最大)特别容易。
数组中的8个成员相加:
unsigned int acc=0;
for (i=0; i<8; i+=1)
{
acc += array[i]; // a + b + c + d + e + f + g + h
}
利用加法的结合律,可以把这个循环展开成几个加法((a+e) + (b+f)) + ((c+g) + (d+h))
:
unsigned int acc1=0;
unsigned int acc2=0;
unsigned int acc3=0;
unsigned int acc4=0;
for (i=0; i<8; i+=4)
{
acc1 += array[i]; // (a, e)
acc2 += array[i+1]; // (b, f)
acc3 += array[i+2]; // (c, g)
acc4 += array[i+3]; // (d, h)
}
acc1+=acc2; // (a + e) + (b + f)
acc3+=acc4; // (c + g) + (d + h)
acc1+=acc3; // ((a + e) + (b + f))+((c + g) + (d + h))
上面的代码展示了一个向量持有4个32位值,用于累加器和临时寄存器。假定相加的数组元素是在32位通道的,然后可以使用SIMD指令进行操作。 将代码扩展为4的任意倍数:
#include
uint32_t vector_add_of_n(uint32_t* ptr, uint32_t items)
{
uint32_t result,* i;
uint32x2_t vec64a, vec64b;
uint32x4_t vec128 = vdupq_n_u32(0); // clear accumulators
for (i=ptr; i<(ptr+(items/4)); i+=4)
{
uint32x4_t temp128 = vld1q_u32(i); // load four 32-bit values
vec128=vaddq_u32(vec128, temp128); // add 128-bit vectors
}
vec64a = vget_low_u32(vec128); // split 128-bit vector
vec64b = vget_high_u32(vec128); // into two 64-bit vectors
vec64a = vadd_u32(vec64a, vec64b); // add 64-bit vectors together
result = vget_lane_u32(vec64a, 0); // extract lanes and
result += vget_lane_u32(vec64a, 1); // add together scalars
return result;
}
vget_high_u32
和vget_low_u32
与任何NEON指令都不相似。这些 intrinsic 指示编译器从输入Q寄存器中引用D寄存器的高位或者低位。因此,这些操作不会转换为实际的代码,但是会影响用于存储vec64a
和vec64b
的寄存器。
根据编译器的版本,目标处理器和优化选项,生成的代码如下:
vector_add_of_n PROC
VMOV.I8 q0,#0
BIC r1,r1,#3
ADD r1,r1,r0
CMP r1,r0
BLS |L1.36|
|L1.20|
VLD1.32 {d2,d3},[r0]!
VADD.I32 q0,q0,q1
CMP r1,r0
BHI |L1.20|
|L1.36|
VADD.I32 d0,d0,d1
VMOV.32 r1,d0[1]
VMOV.32 r0,d0[0]
ADD r0,r0,r1
BX lr
ENDP
一些指令没有对等的 intrinsics
大多数的NEON指令都有等效的intrinsics,但是下面的没有:
VSWP
, VLDM
, VLDR
, VMRS
,VMSR
, VPOP
, VPUSH
, VSTM
, VSTR
, VBIT
, VBIF
注意:
无法明确生成VBIF
和VBIT
,但是 intrinsics VBSL
可以生成任意VBSL
、VBIT
或者VBIF
指令。
VSWP
指令没有对应的intrinsics,因为编译器可以在需要的时候生成VSWP
指令,例如用C风格变量分配来交换变量的时候。
VLDM
,VLDR
,VSTM
和VSTR
主要用于上下文切换,这些指令具有对齐约束。 编写 intrinsics 时,使用vldx
intrinsics更简单。除非明确指定,否则vldx
intrinsics不需要对齐。
VMRS
和VMSR
访问NEON的条件标志,数据处理的时候不需要用NEON intrinsics
VPOP
和VPUSH
用于传递给函数的参数中。减少变量重用,或使用更多NEON intrinsics 变量,可使寄存器分配器保持对活动的寄存器的跟踪。