由一个简单例子上手neon

    我也是纯新手,看了这篇博客http://blog.csdn.net/may0324/article/details/72847800,感觉对上手很有帮助。本文也是由这篇博客给的例子出发,探讨一些问题。

    1、编译

    要让我们的代码在板子上跑,必然是要用交叉工具链来编译,我自己用的是arm-linux-g++。在编译neon优化的代码时,要加入 -mfloat-abi=softfp -mfpu=neon -ffast-math。-mfloat-abi有三个可选项:可选soft/softfp/hardfp: soft和后两者的区别是编译器不会生成浮点指令,浮点操作完全由软件实现;相比之下,hardfp走了另一个极端,浮点操作完全由硬件实现,效率最高;softfp则是走中间路线,它有着和hardfp完全不一样的calling convention——hardfp通过VFP来传浮点参数,而softfp还是通过整型寄存器来传参,和soft是一致的。如果选择soft,那么就不支持neon了。需要-mfpu=neon来指定FPU(Floating-Point Unit)是NEON Unit;-ffast-math是浮点优化选项,可以极大提高浮点运算速度。
   此外,使用neon指令需要头文件。

2、代码部分

还是先贴一下上述博客中的代码(稍作修改)。代码中使用的各函数作用可参见上述博客。
#include
#include
using namespace std;
float sum_array(float* arr,int len);

int main()
{ 
  long l=100000;
  float array[l];
  for(long i=0;i
首先是普通的c++代码(记为no_neon),
#include
#include
#include
using namespace std;
float sum_array(float* arr,int len);

int main()
{ 
  long l=100000;
  float array[l];
  for(long i=0;i0;dim4--,arr+=4)
    {
      float32x4_t data_vec = vld1q_f32(arr);
      sum_vec = vaddq_f32(sum_vec,data_vec);
    }
  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);
    }
   
  return sum;

}
这是上面博客中作者给出的neon优化后的代码(记为neon)。功能很简单,就是一个10万个浮点数据的数组求和。
我修改的部分就是将neon优化的部分迭代1000次,记录平均耗时。
这两个代码在板子上运行的结果如下:
由一个简单例子上手neon_第1张图片
可见,用neon优化后的代码运行时间降低了约50%,但是理论上说,寄存器一次性处理一组4维向量的运算,应该能提高4倍运行速度。这个原因,我也不懂。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------分割线
之后,我想能不能进一步优化呢?首先想到的就是把数组的长度补到4的整数倍,这样就能把上面neon中最后left4的那部分并入neon的方法中。于是就有了如下代码:
#include
#include
#include
using namespace std;
float sum_array(float* arr,int len);

int main()
{ 
  long l=100000;
  float array[l];
  for(long i=0;i if(l%4!=0) 
   {
    int a=4-l%4;
    l+=a;
    for(int i=1;i<=a;i++)
     {
      array[l-i]=0;
     } 
   }
  clock_t starttime=clock();
  for(int round=1;round<=1000;round++)
  {
  float Sum=sum_array(array,l);
  //cout<0;dim4--,arr+=4)
    {
      float32x4_t data_vec = vld1q_f32(arr);
      sum_vec = vaddq_f32(sum_vec,data_vec);
    }
  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);
   
  return sum;

}
记为neon1,红字部分就是扩展数组长度,并将扩展的位赋0 。测试结果:

时间反而比之前的neon长了。回头检查发现问题所在,10万本就是4的倍数,修改的代码并没有发挥作用,于是将10w改为10w+7,再次测试,结果如下:
由一个简单例子上手neon_第2张图片
这时我们就发现,当数组长度不是4的倍数,neon代码时间有所增加,而neon1代码耗时降低。

3、结构化输入

    之后,我走进了一个误区,认为一次性写入寄存器的数据越多,代码跑起来就越快。虽然这里方向走错了,但是也不妨碍学习一下结构化输入,这到了图像处理中应该会经常用到。之前学习相应的硬件知识我们知道,neon提供16个128bit寄存器,如何一次给多组寄存器输入呢?
    我们用  float32x4x4_t  来定义一个占用4个128bit寄存器的量。相应的从指针读取数值的指令为  vld4q_f32(float32_t const* ptr), 如下图,假设我们占用的寄存器为q1-q4,用vld4q时,arr[0]存入q1第一格(形象化的说法),arr[1]存入q2第一格,arr[2]存入q3第一格......以此类推,而不是按顺序依次填满每个寄存器。这为我们分图层处理图像提供了极大便利。
由一个简单例子上手neon_第3张图片
以下贴出一次读取16个数的部分代码,记为neon2,(之前也做了数组扩展)
 clock_t starttime=clock();
 for(int round=1;round<=1000;round++)
 { 
  float32x4x4_t data_vec;
  float32x4_t sum_vec = vdupq_n_f32(0.0);
  for(long i=0;i+16<=l;i+=16)
   {
    data_vec=vld4q_f32(arr+i);
    for(int j=0;j<4;j++)
     {
      sum_vec = vaddq_f32(sum_vec,data_vec.val[j]);
     }
   }
  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);
  //cout<
其中data_vec.val[j]表示结构化输入占用的一个寄存器。
我原本以为这样可以进一步加速,但是测试结果为:


反而时间还变长了不少。 究其原因,我们考虑优化、加速的问题应该从机器的角度来考虑代码是如何执行的,也就是汇编语言,吧。在处理加法问题时,最基本的操作是把两个寄存器对应元素相加,值赋给制定寄存器相应的位置。也就是说不论一次给几个寄存器赋值,最后寄存器之前做加法的次数并没有减少,甚至可能因为扩展数组的操作,增加了运算的次数。也不知道我这样理解对还不对哈。

参考文献

http://blog.csdn.net/may0324/article/details/72847800
https://segmentfault.com/a/1190000010127521
https://developer.arm.com/technologies/neon/intrinsics

你可能感兴趣的:(由一个简单例子上手neon)