ARM的NEON指令优化

原文链接: https://blog.csdn.net/fuwenyan/article/details/78793907

本文参考整理了以下文章:

http://blog.csdn.net/may0324/article/details/72847800

http://blog.csdn.net/chshplp_liaoping/article/details/12752749

http://blog.csdn.net/charleslei/article/details/52698220

 

在移动平台上进行一些复杂算法的开发,一般需要用到指令集来进行加速。NEON 技术是 ARM Cortex™-A 系列处理器的 128 位 SIMD(单指令,多数据)架构扩展,专门针对大规模并行运算设计的,旨在为消费性多媒体应用程序提供灵活、强大的加速功能,从而显著改善用户体验。

目前主流的iPhone手机和大部分android手机都支持ARM NEON加速,因此在编写移动端算法时,可利用NEON技术进行算法加速,以长度为4的寄存器大小为例,相应的提速倍数约是原始的4倍。

 

 

NEON的寄存器:

NEON SIMD 寄存器的长度为 128 位,如果操作 32 位浮点数,可同时操作 4 个;如果操作 16 位整数(short),可同时操作 8 个;而如果操作 8 位整数,则可同时操作 16 个。ARMv7 NEON 指令集架构具有 16 个 128 位的向量寄存器,命名为 q0~q15。这 16 个寄存器又可以拆分成 32 个 64 位寄存器,命名为 d0~d31。其中qn和d2n,d2n+1是一样的,故使用汇编编写代码时要注意避免产生寄存器覆盖。

有16个128位四字到寄存器Q0-Q15,32个64位双子寄存器D0-D31,两个寄存器是重叠的,在使用到时候需要特别注意,不小心就会覆盖掉。如下图所示:

两个寄存器的关系:Qn =D2n和D2n+1,如Q8是d16和d17的组合。

 ARM的NEON指令优化_第1张图片

NEON的数据类型:

注意数据类型针对到时操作数,而不是目标数,这点在写的时候要特别注意,很容易搞错,尤其是对那些长指令宽指令的时候,因为经常Q和D一起操作。

 ARM的NEON指令优化_第2张图片

 

neon的数据类型float32x4_t 可以理解为vector> (4),同理typexN_t即为vector>(N)。在NEON编程中,对单个数据的操作可以扩展为对寄存器,也即同一类型元素矢量的操作,因此大大减少了操作次数。

NEON中的正常指令、宽指令、窄指令、饱和指令、长指令

正常指令:生成大小相同且类型通常与操作数向量相同到结果向量

长指令:对双字向量操作数执行运算,生产四字向量到结果。所生成的元素一般是操作数元素宽度到两倍,并属于同一类型。L标记,如VMOVL。

宽指令:一个双字向量操作数和一个四字向量操作数执行运算,生成四字向量结果。W标记,如VADDW。

窄指令:四字向量操作数执行运算,并生成双字向量结果,所生成的元素一般是操作数元素宽度的一半。N标记,如VMOVN。

饱和指令:当超过数据类型指定到范围则自动限制在该范围内。Q标记,如VQSHRUN

 

使用 NEON 指令读写数据时,不需要保证数据对齐到 16 字节。GCC 支持的 NEON 指令集的C 语言接口(内置函数,intrinsic)声明在 arm_neon.h 头文件中。 NEON 指令集支持的映射到向量寄存器的向量数据类型命名格式为type size x num。

其中:

①type表示元素的数据类型,目前只支持 float、int和uint。

②size表示每个元素的数据长度位数,float 只支持 32 位浮点数,int 和 unit 支持 8 位、16 位、 32 位和 64 位整数。

③num表示元素数目,即向量寄存器的位数。由于NEON只支持 64 位和 128 位向量寄存器,故size和num的乘积只能是64或128。

如uint16x8_t表示每个元素数据类型为uint,大小为16位,每个向量保存8个数,故使用的向量寄存器长度为128位;如float32x4_t表示每个元素的数据类型为32位浮点,向量寄存器可操作4个数据,故使用128位向量寄存器。

NEON 内置函数命名方式有两种,分别对应源操作数是否涉及标量,具体解释如下。

1)源操作数涉及标量时,数据类型表示为v op dt_n/lane_type。

其中:

①n表示源操作数是标量而返回向量,lane 表示运算涉及向量的一个元素。

②op表示操作,如dup、add、sub、mla等。

③dt是目标向量和源向量长度表示符。

如果目标向量和源向量长度都为64位,dt为空。

如果源向量和目标向量长度一致都为128位,dt为q。

如果目标向量长度比源数向量长度大,且源向量长度都为 64 位、目标向量长度为 128 位,dt为 l(英文字母,不是数字1)。

如果多个源向量长度不一致且都不大于目标向量长度(一个源向量长度为 64 位,另一个为 128 位,目标向量长度为 128 位),dt为 w。

如果目标向量长度比源向量长度小,即目标向量长度dt为 n。

④type表示源数据类型缩写,如u8 表示 uint8;u16 表示 uint16;u32 表示 uint32;s8 表示 int8;s16 表示 int16;s32 表示 int32;f32 表示 float32。

2)源操作数全是向量时,数据类型表示为v op dt_type,其中op、dt和type的含义和源操作数为标量时一致。

下面给出几个实例以增加读者理解。

1)内置函数vmla_f32表示使用64位向量寄存器操作32位浮点数据,即源操作数使用的向量寄存器和目标操作数使用的向量寄存器表示都是float32x2_t。

2)内置函数vmlaq_f32表示使用128位向量寄存器操作32位浮点数据,即源操作数使用的向量寄存器和目标操作数使用的向量表示都是float32x4_t。

3)内置函数vmlal_u32表示使用的目标寄存器是128位向量,源寄存器是64位向量,操作32位无符号整数。

4)内置函数vaddw_s32表示使用的目标寄存器是128位向量,源寄存器一个是64位向量,一个是128位向量。

5)内置函数vmovn_u64表示目标寄存器是64位向量,源寄存器是128位向量,即同时操作两个数。

 

这里以一个小例子来解释如何利用NEON内置函数来加速实现统计一个数组内的元素之和。

以C++代码为例: 
原始算法代码如下:

#include 
using namespace std;
 
float sum_array(float *arr, int len)
{
    if(NULL == arr || len < 1)
    {
        cout<<"input error\n";
        return 0;
    }
    float sum(0.0);
    for(int i=0; i

对于长度为N的数组,上述算法的时间复杂度时O(N)。 
采用NEON函数进行加速:

#include 
#include  //需包含的头文件
using namespace std;
 
float sum_array(float *arr, int len)
{
    if(NULL == arr || len < 1)
    {
        cout<<"input error\n";
        return 0;
    }
 
    int dim4 = len >> 2; // 数组长度除4整数
    int left4 = len & 3; // 数组长度除4余数
    float32x4_t sum_vec = vdupq_n_f32(0.0);//定义用于暂存累加结果的寄存器且初始化为0
    for (; dim4>0; dim4--, arr+=4) //每次同时访问4个数组元素
    {
        float32x4_t data_vec = vld1q_f32(arr); //依次取4个元素存入寄存器vec
        sum_vec = vaddq_f32(sum_vec, data_vec);//ri = ai + bi 计算两组寄存器对应元素之和并存放到相应结果
    }
    float sum = vgetq_lane_f32(sum_vec, 0)+vgetq_lane_f32(sum_vec, 1)+vgetq_lane_f32(sum_vec, 2)+vgetq_lane_f32(sum_vec, 3);//将累加结果寄存器中的所有元素相加得到最终累加值
    for (; left4>0; left4--, arr++)
        sum += (*arr) ;   //对于剩下的少于4的数字,依次计算累加即可
    return sum;
}

上述算法的时间复杂度时O(N/4) 
从上面的例子看出,使用NEON函数很简单,只需要将依次处理,变为批处理(如上面的每次处理4个)。

上面用到的函数有: 
float32x4_t vdupq_n_f32 (float32_t value) 
将value复制4分存到返回的寄存器中

float32x4_t vld1q_f32 (float32_t const * ptr) 
从数组中依次Load4个元素存到寄存器中

相应的 有void vst1q_f32 (float32_t * ptr, float32x4_t val) 
将寄存器中的值写入数组中

float32x4_t vaddq_f32 (float32x4_t a, float32x4_t b) 
返回两个寄存器对应元素之和 r = a+b

相应的 有float32x4_t vsubq_f32 (float32x4_t a, float32x4_t b) 
返回两个寄存器对应元素之差 r = a-b

float32_t vgetq_lane_f32 (float32x4_t v, const int lane) 
返回寄存器某一lane的值

其他常用的函数还有:

float32x4_t vmulq_f32 (float32x4_t a, float32x4_t b) 
返回两个寄存器对应元素之积 r = a*b

float32x4_t vmlaq_f32 (float32x4_t a, float32x4_t b, float32x4_t c) 
r = a +b*c

float32x4_t vextq_f32 (float32x4_t a, float32x4_t b, const int n) 
拼接两个寄存器并返回从第n位开始的大小为4的寄存器 0<=n<=3 
例如 

a: 1 2 3 4 
b: 5 6 7 8 
vextq_f32(a,b,1) -> r: 2 3 4 5 
vextq_f32(a,b,2) -> r: 3 4 5 6 
vextq_f32(a,b,3) -> r: 4 5 6 7

float32x4_t sum = vdupq_n_f32(0);
float _a[] = {1,2,3,4}, _b[] = {5,6,7,8} ;
float32x4_t a = vld1q_f32(_a), b = vld1q_f32(_b)  ;
float32x4_t sum1 = vfmaq_laneq_f32(sum, a, b, 0);
float32x4_t sum2 = vfmaq_laneq_f32(sum1, a, b, 1);
float32x4_t sum3 = vfmaq_laneq_f32(sum2, a, b, 2)

neon各种指令的官方检索和解释可以参考:

https://developer.arm.com/technologies/neon/intrinsics

你可能感兴趣的:(编程)