天天看点

NEON Programmer's Guide(2) —— 让编译器帮你实现NEON加速简介NEON库让编译器将代码编译成NEON指令

本文内容基于《NEON Prgrammer’s Guide》(下称NPG)第二章Compiling NEON Instruction,所有编译命令都基于gcc-arm

简介

让你的程序使用arm内核的NEON unit进行并行计算,提升嵌入式程序性能,一般有3种方法:

  1. 手撸NEON代码,无论是汇编还是Intrinsics
  2. 使用NEON库
  3. 让编译器将代码编译成NEON指令

第1项是NPG的正题,以后再说,这一章还是说一些简单的,比如第2项和第3项。

NEON库

NPG中推荐了几个开源的利用NEON进行优化的函数库(不过我看的是2013年的NPG,是不是后来又出了其他库就不知道了),有需要百度库名即可:

  1. Ne10,将NEON指令封装成了C语言函数,实现了一些基础数学和dsp运算,估计以后会用,用的时候再介绍
  2. OpenMAX,open media acceleraion,一个多媒体加速库,有为ARM实现的NEON加速版本
  3. ffmpeg,搞音视频的应该比较熟悉,一个音视频编解码库。
  4. Eigen3,一个线性代数的C++库,做图像或者机器人的朋友应该常用
  5. Pixman,一个2D图形图像函数库
  6. x264,一个H.264的图像编码库
  7. Math-noen,另外一个数学操作函数库
  8. libjpeg-turbo,这个书里没有,是我刚刚用过的jpeg图像加速编解码库

让编译器将代码编译成NEON指令

NGP书中,一般用vectorization来表示使用NEON指令来进行并行计算加速,所以让编译器来帮我们进行vectorization就叫做auto-vectorization。一般有3个步骤:

确定你的处理器有NEON unit

linux环境下命令行输入

cat /proc/cpuinfo | grep neon

,如果有输出

Features : swp half thumb fastmult vfp edsp thumbee neon vfpv3

类似的输出,恭喜你,你的cpu有neon unit。如果没有,那么我也没啥办法,查查cpu的datasheet吧。

不过要注意的是,NEON unit在开机了之后是默认disable的(上述指令表示你的cpu有NEON unit这个硬件)。

但是Linux系统会在执行程序的时候,第一次遇到NEON指令时,会使能NEON unit,直至遇到上下文切换。所以使用Linux的同学应该不用担心。如果自己编译内核的同学,则需要注意一下,在编译过程中需要在Floating point emulation选项下打开VFP-fomat floating point maths和Advanced SIMD(NEON) Extension support。

对于使用其他OS或者裸机跑程序(ARM裸机,这个有点虎)的同学,则需要手动使能NEON unit,

// Bare-minimum start-up code to run NEON code
__asm void EnableNEON(void)
{
	MRC p15,0,r0,c1,c0,2 // Read CP Access register
	ORR r0,r0,#0x00f00000 // Enable full access to NEON/VFP by enabling access to
	 // Coprocessors 10 and 11
	MCR p15,0,r0,c1,c0,2 // Write CP Access register
	ISB
	MOV r0,#0x40000000 // Switch on the VFP and NEON hardware
	MSR FPEXC,r0 // Set EN bit in FPEXC
}
           

写出能被vectorization的代码

NPG中提供了一些原则,可以帮助我们的code更好的被编译器实现vetorization,

  1. 让循环变得尽可能的简单和短,比如说,
int a[10]={0}, b[10]={0}, c[10]={0};
// ×
for (int i=0; i<10; i++)
{
	a[i] += 1;
	b[i] += 2;
	c[i] += 3;
}
// √
for (int i=0; i<10; i++)
	a[i] += 1;
for (int i=0; i<10; i++)
	b[i] += 2;
for (int i=0; i<10; i++)
	c[i] += 3;
	
// 其实上面这样写的目的是为了在一个循环里面可以连续寻址,加载进NEON寄存器,
// 所以下面这种写法就不如合起来写
struct rgb {char r; char g; char b};
struct rgb out[10];
for (...) 
	out[i].r = ...;
for (...) 
	out[i].g = ...;
for (...) 
	out[i].b = ...;
           
  1. 在循环中避免使用break和if
  2. 尽量让循环次数时2的幂
  3. 尽量让编译器能知道要循环多少次,比如说
// 编译器对循环次数一无所知
void func(int n)
{
	for (int i=0; i<n; i++)
	{
		...
	}
}
// 编译器知道循环次数是4的倍数
void func(int n)
{
	for (int i=0; i<n & (~3); i++)
	// or for (int i=0; i<n*4; i++)
	{
		...
	}
}
           
  1. 循环内的函数调用尽量使用inline
  2. 多用数组来indexing,而不要用指针
  3. 不要用Indirect Addressing(这个我也没懂,反正应该不是寄存器的简介寻址)
  4. 使用restrict关键字让编译器知道code中没有使用重叠的内存区域
  5. 关于结构体,NPG中说了两种不好的写法,
// 有的同学会为了对齐到32-bit在下面结构体的最后加上一个not_used的字段,
// 但是在循环中没有用到not_used字段,会打断第1点中说的NEON连续寻址,所以编译器也不会做auto-vectorization
struct aligned_pixel 
{ 
	char r; 
	char g;
	char b; 
	char not_used; /* Padding used to keep r aligned to a 32-bit word */
}screen[10];
// 这个就更不用说了, 数据类型都不一样,编译器会说我好难。
// 不缺内存的情况下,可以将结构体中的数据类型都对齐到最宽的数据类型
struct pixel 
{ 
char r; 
short g; /* Green channel contains more information */ 
char b; 
}screen[10];
           

NPG中在好多地方分散说了很多进行vectorization的代码注意要点,我这边总结了我认为比较重要的部分,但难免有些细节遗漏,如果有兴趣,可以深入看看NPG第2章。其实总结起来,无非是如果你的code你自己要做vectorization都觉得困难,那就别指望编译器了。

利用gcc进行vectorization

编译使用选项——

gcc/g++ xxx.c -o target -ftree-vectorize -mfpu=neon -mcpu=cortex-aN

其中,

  1. -ftree-vectorize

    表示让编译器进行vectorization。在我的实际使用中,感觉没啥效果,要直接

    -O3

    才能vectorization
  2. -mfpu

    指定FPU。对于我使用的cortex-a7,NPG推荐使用

    -mfpu=neon-vfpv4

    (不过只用

    neon

    好像也能用…)
处理器 -mfpu
cortex-a5 neon-fp16
cortex-a7 neon-vfpv4
cortex-a8 neon
cortex-a9 neon-fp16
cortex-a15 neon-vfpv4
  1. -mcp

    指定cpu型号。