Table of Contents
NEON技术
关于ARM处理器NEON技术的介绍可以参考维基百科、百度百科词条、ARM官网,关于NEON技术的使用可以参考网上的各种博客,但是似乎来自于ARM官网的这份NEON Programmer’s Guide(或者从本网站直接下载)最为全面可靠。
NEON技术简介
处理器处理数据的指令大致有三种:
- Single Instruction Single Data 一个指令的源操作数和目的操作数都是唯一的。这里的唯一是指源操作数和目的操作数都是标量(scalar)/单个的数据。
比如add r0, r5
这种指令所做的就仅仅是把r0,r5这两个寄存器里的东西加起来。 - Single Instruction Multiple Data(vector mode) 一个指令的源操作数和目的操作数是向量(或者说数组),该指令会依次/顺序为数组的每个元素执行相应的操作。
比如VADD.F32 S24, S8, S16
这条指令实际上会依次执行下面四个操作:S24 = S8 + S16 S25 = S9 + S17 S26 = S10 + S18 S27 = S11 + S20
- Single Instruction Multiple Data(packed data) 一个指令的源操作数和目的操作数是向量,该指令会同时为数组的每个元素执行相应操作。由于这个同时,因此这种指令就比上面第二种顺序执行的向量模式的SIMD更有效率。至于所谓的packed data的含义是这种指令的源操作数和目的操作数必须放在特殊的寄存器(所谓的D寄存器和Q寄存器)里才能进行这种运算。ARM称这种技术为Advanced SIMD technology或者NEON technology。
在C/C++中使用NEON技术
首先确定能否使用NEON技术
- ARMv7开始的处理器才有NEON技术,可以通过
/proc/cpuinfo
检查一下处理器是否带neon扩展就行; - 通过在代码中检查编译链是否具有
__ARM_NEON__
这个预定义宏确认是否可以使用NEON技术,即代码应该是这样的:#ifdef __ARM_NEON__ //使用了NEON技术的代码 #else //没有使用NEON技术的代码 #endif
在C/C++中使用NEON技术
NEON技术原理
首先叙述一下我所理解的NEON技术,在ARM处理器存在着所谓的D寄存器和Q寄存器。D寄存器长度为64位,可以存2个32位长度的数据类型(如int_32/uint_32)或者4个16位的数据类型或者8个8位的数据类型(总之就是一样的数据类型凑齐64位),这寄存器里的每个数据类型叫做一个lane(或者说对应一个lane吧)。Q寄存器也是类似的,只是Q寄存器长度更长,有128位而已。D寄存器和Q寄存器里的数据就可以用来执行一开始讲的SIMD(packed data)这种高效率指令。比如两个D寄存器D1,D2都包含8个uint_8的数据,在将这两个寄存器执行相加操作时,处理器会把两个处理器对应位置上的uint_8相加,然后存到目的寄存器比如D3里。比如D1的第一个uint_8和D2的第一个uint_8相加,结果存到D3的第一个uint_8里,D1的第二个uint_8和D2的第二个uint_8相加,结果存到D3的第二个uint_8里,以此类推。
所以,只要我们能够把我们要作运算的数据给放入D或者Q寄存器里,执行相应的运算,再把数据从D或者Q寄存器里取出来,就达到了使用NEON技术加速(加速不加速还得看实际效果)运算的目的。
使用NEON Intrinsics
最直接的使用NEON技术的方式当然是使用汇编了,但是没多少人有兴趣和能力去折腾汇编吧,或者人手写出来的汇编不一定真有程序自动生成的好。为了方便C/C++程序员也能使用NEON技术,于是就有了NEON Intrinsics存在的需求。我不知道NEON Intrinsics的中文翻译是什么,大致就是内联函数一样的东西,通过类似函数一样的接口去调用相关的NEON指令。
包含必要的头文件
想要使用NEON Intrinsics需要包含arm_neon.h这个头文件。
将数据放入D或者Q寄存器
只有特殊数据类型的变量才可以放入D或者Q寄存器里进行操作,这些特殊的数据类型是这样子的:<type><size>x<number_of_lanes>_t
。
比如:
- int16x4_t表示4个16位的short,这4个16位的short可以一起放在一个D寄存器中
- float32x4_t表示4个32位的float,这4个float可以一起放在一个Q寄存器中
在初始化上述数据类型的时候,是不能直接初始化的,需要调用相应的NEON Intrinsics函数。
NEON Intrinsics函数是这样子的:<opname><flags>_<type>
opname是运算或者操作类型,flags如果没有的话,就表示一般的、在D寄存器上进行的操作,有的话,一般是q(表示是对Q寄存器进行操作)或者l(结果数据类型比源操作数数据类型长)之类的值。type表示寄存器里单个数据类型(每个lane的数据类型)。
比如:
- vmul_s16是将两个D寄存器里对应的short相乘
- vaddl_u8把两个D寄存器里的uint8x8_t相加,结果给放在了Q寄存器的uint16x8_t里面。
回到具体的初始化这些特殊类型变量的问题:
- 将初始值都设为0
uint8x8 a; a = vdup_n_u8(0);
- 将初始值设为特定的值
uintix8 a = vreinterpret_u8_u64(vcreate_u64(0x123456789ABCDEFULL));
- 设置某个lane的值 使用
VSET_LANE
:Result_t vset_lane_type(Scalar_t N, Vector_t M, int n); Result_t vsetq_lane_type(scalar_t N, Vector_t M, int n); - 根据已有某个数组的值/某块内存初始化 使用
VLD1
Intrinsic:Result_t vld1_type(Scalar_t *N); Result_t vld1q_type(Scalar_t *N);
进行所需要的运算
比如对于三个int16x4_t
类型的变量sum
,a
,x
,要计算sum = a * x + sum
,可以这样写:
sum = vmla_s16(sum, a, x);
这完全取决于自己的需要而选择合适的NEON Intrinsic。
从D或者Q寄存器中取出运算结果
- 可以使用
VST1
Intrinsic把结果存到内存中:void vst1_type(Scalar_t *N, Vector_t M); void vst1q_type(Scalar_t *N, Vector_t M); - 也可以用
VGET_LANE
Intrinsic直接获取每一个lane的值:Result_t vget_lane_type(Vector_t N, int n); Result_t vgetq_lane_type(Vector_t N, int n);
编译器选项设置
- 在编译器的命令选项中指定要使用NEON技术:
-mfpu=neon
-mcpu=<一个支持NEON的处理器>
-ftree-vectorize
-mfloat-abi=softfp
-mfloat-abi
选项有三个可取的值,soft,softfp,hard,我只用过softfp,关于这三个值的具体含义可以查看manual。
-ftree-vectorize
这个选项,使用-O3
选项就已经默认包含了这个选项。
一个矩阵乘法的例子
最后直接贴上一个NEON加速的矩阵乘法的例子:
void neon_float_matrix_vec_mul(const float *matrix, const float *vec,
float *vec_ret, int row, int col) {
float32x4_t sum, a, x;
for (int i = 0; i < row; i++) {
sum = vdupq_n_s32(0);
for (int j = 0; j < col; j += 4) {
a = vld1q_f32(matrix + i*col + j);
x = vld1q_f32(vec + j);
sum = vmlaq_f32(sum, a, x);
}
vec_ret[i] = vgetq_lane_f32(sum, 0)
+vgetq_lane_f32(sum, 1)
+vgetq_lane_f32(sum, 2)
+vgetq_lane_f32(sum, 3);
}
}
如果需要处理图像或者声音这种多路数据交替存储在一块的东西,可以参考手册中Constructing multiple vectors from interleaved memory这一部分内容。
近期评论