ARM平台NEON指令的编译和优化
ARM平台NEON指令的编译和优化
  本⽂介绍了ARM平台基于ARM v7-A架构的ARM Cortex-A系列处理器(Cortex-A5, Cortex-A7,Cortex-A8, Cortex-A9, Cortex-
A15)上的NEON多媒体处理硬件加速器针对C/C++语⾔、汇编语⾔和NEON intrinsics如何编译和优化,包含如何向量化、向量化的ARMCC和GCC编译器选项、NEON的汇编和EABI程序调⽤规范、如何在bare-metal和Linux操作系统上检测NEON硬件、如何指导编译器进⾏向量化NEON指令的优化等内容。
NEON向量化
  基于ARM v7-A架构的ARM Cortex-A系列处理器(Cortex-A5, Cortex-A7, Cortex-A8, Cortex-A9, Cortex-A15)都可以选⽤NEON多媒体处理器加速程序运⾏,NEON是⼀种SIMD(Single Instruction Multiple Data)架构的协处理器,ARM的NEON处理器还可选配置成向量浮点VFPv3(Vector Floating-Point)指令集处理器。
常⽤的编译器选项配置
⾃动向量化选项
  armcc编译器使⽤–vectorize选项来使能向量化编译,⼀般选择更⾼的优化等级如-O2或者-O3就能使能–vectorize选项。
  gcc编译器的向量化选项-ftree-vectorize来使能向量化选项,使⽤-O3会⾃动使能-ftree-vectorize选项。
选择处理器类型
  armcc编译器使–cpu 7-A或者–cpu Cortex-A8来指定指令集架构和CPU类型。
  gcc编译器的处理器选项-mfpu=neon和-mcpu来指定cpu类型。如-mcpu=cortex-a5
选择NEON和VFP类型
  gcc选择⽤-mfpu=vfpv3-fp16来指定为vfp协处理,⽽-mfpu=neon-vfpv4等就能指定为NEON+VFP结构。
选择浮点处理器和ABI接⼝类型
  -mfloat-abi=soft使⽤软件浮点库,不是⽤VFP或者NEON指令;-mfloat-abi=softfp使⽤软件浮点的调⽤规则,⽽可以使⽤VFP和NEON指令,编译的⽬标代码和软件浮点库链接使⽤;
  -mfloat-abi=hard使⽤VFP和NEON指令,并且改变ABI调⽤规则来产⽣更有效率的代码,如⽤vfp寄存器来进⾏浮点数据的参数传递,从⽽减少NEON寄存器和ARM寄存器的拷贝。
常⽤的CPU类型编译器选项
CPU类型CPU类型选项FP选项FP + SIMD选项备注
Cortex-A5-mcpu=cortex-a5
-mfpu=vfpv3-fp16
-mfpu=vfpv3-d16-fp16
-mfpu=neon-fp16-d16表明只有前16个浮点寄存器可⽤
Cortex-A7-mcpu=cortex-a7
-mfpu=vfpv4
-mfpu=vfpv4-d16
-mfpu=neon-vfpv4-fp16表明⽀持16bit半精度浮点操作
Cortex-A8-mcpu=cortex-a8-mfpu=vfpv3-mfpu=neon
Cortex-A9-mcpu=cortex-a9
-mfpu=vfpv3-fp16
-mfpu=vfpv3-d16-fp16
-mfpu=neon-fp16
Cortex-A15-mcpu=cortex-a15-mfpu=vfpv4-mfpu=neon-vfpv4常⽤的gcc组合编译器选项
Cortex-A15 with a NEON unit
arm-gcc-O3-mcpu=cortex-a15-mfpu=neon-vfpv4-mfloat-abi=hard - myprog.c
Cortex-A9 with a NEON unit
arm-gcc-O3-mcpu=cortex-a9-mfpu=neon-vfpv3-fp16-mfloat-abi=hard - myprog.c
Cortex-A7 without a NEON unit
arm-gcc-O3-mcpu=cortex-a7-mfpu=vfpv4-d16-mfloat-abi=softfp - myprog2.c
Cortex-A8 without a NEON unit
arm-gcc-O3-mcpu=cortex-a8-mfloat-abi=soft -file.c
NEON汇编和EABI程序调⽤规范
  GNU assembler (gas) and ARM Compiler toolchain assembler(armasm)都⽀持NEON指令的汇编。但必须遵循ARMEmbedded Application Binary Interface (EABI)EABI的规范,即NEON寄存器的S0-S15 (D0-D7, Q0-Q3)⽤于传递参数和返回值,被调⽤函数内可以直接使⽤,不⽤保存;D16-D31 (Q8-Q15)则有调⽤函数来保存,被调⽤函数内可以不保存的随意使⽤;⽽S16-S31(D8-D15, Q4-Q7)则必须由被调⽤函数内部保存。对于调⽤传参规范则有,对于软件浮点,参数有R0~R3和堆栈stack传递,⽽硬件浮点,可以通过NEON寄存器来传递参数。
NEON硬件检测和使能
编译时指定NEON单元是否存在
  ARM编译器(armcc)从4.0之后就⽀持在某些处理器和FPU的选项中预定义宏ARM_NEON, armasm的宏
TARGET_FEATURE_NEON.
运⾏时指定检测NEON单元
  OS内可以检测NEON单元是否存在,如Linux下cat /proc/cpuinfo看是否包含NEON或者VFP,
  如Tegra2 (双核 Cortex-A9 带 FPU),
# cat /proc/cpuinfo
Features : swp half thumb fastmult vfp edsp thumbee vfpv3vfpv3d16
  四核 Cortex-A9 带NEON单元
# cat /proc/cpuinfo
Features : swp half thumb fastmult vfp edsp thumbee neonvfpv3
  另外,可以查看/proc/self/auxv,这⾥会包含⼆进制格式的hwcap,可以通过AT_HWCAP来搜索到。HWCAP_NEON bit (4096).另外如Ubuntu的发布在路径/lib/neon/vfp下包含lib的NEON优化版本。
Bare-metal模式下使能NEON
#include <stdio.h>
// Bare-minimum start-up code to run NEON code
__asm void EnableNEON(void)
{
MRC p15,0,r0,c1,c0,2// Read CPAccess register
ORR r0,r0,#0x00f00000  // Enablefull access to NEON/VFP by enabling access to
// Coprocessors 10 and 11
MCR p15,0,r0,c1,c0,2// Write CPAccess registerISB
MOV r0,#0x40000000      // Switch onthe VFP and NEON hardware
MSR FPEXC,r0            // Set EN bit inFPEXC
gnu编译器}
  下⾯的EnableNEON函数使能NEON协处理器;使⽤下⾯的编译选择就能在bare-metal下使能NEON
armcc -c --cpu=Cortex-A8 --debug hello.c -o hello.o
armlink --entry=EnableNEON hello.o -o hello.axf
系统运⾏时使能NEON
  内核在遇到第⼀个NEON指令时会产⽣⼀个UndefinedInstruction的异常,这会让内核⾃动重启NEON协处理器,内核还可以在上下⽂切换时关闭NEON来省电。
Linux内核的NEON 配置
          图1. NEON的Linux内核配置
  使能NEON,需要选择以下选项
  Floating point emulation → VFP-format floating point maths
  Floating pointemulation → Advanced SIMD (NEON) Extension
  检查Linux的配置⽂件来确认内核是否使能NEON
  zcat / | grep NEON
  看是否存在
  CONFIG_NEON=y
  确认处理器是否⽀持NEON
  cat /proc/cpuinfo | grep neon
  看是否有如下内容
  Features : swp half thumb fastmult vfp edsp neon vfpv3 tlsvfpv4 idiva idivt
向量化NEON优化指南
避免指针混叠alias
  C90不要求指针位置,不同指针可以指向相同的内存区域,C99中引⼊了__restrict关键字来表明只有这个指针能指向它⼯作的区域。告诉编译器循环信息
  如循环是否某个整数的整数倍,以⽅便向量化;如下表明循环次数是4的整数倍:
for(i=0 ; i < (len & ~3) ; i++)
{
...
}
for (i=0; i<(items*4); i+=1)
{
...
}
循环展开
#pragma unroll (n)
采⽤NEON Intrinsics
  armcc, GCC/g++和llvm等编译器都⽀持 NEON C/C++ intrinsics,并且采⽤相同的语法规范。因⽽代码可以在各个编译器间共享。NEON Intrinsics的代码容易维护⽽且效率⾼。NEON Intrinsics采⽤新的数据类型,这些类型对应于D和Q寄存器。NEONIntrinsics写起来像是函数调⽤但对应于每⼀条NEON指令。编程NEON Intrinsics时不⽤考虑具体的寄存器分配和代码的schedule,pipeline流⽔安排等。但NEON Intrinsics往往不能产⽣想象的代码,性能上相⽐纯汇编要稍差⼀些。
减少循环内的相关性
  如果当前迭代时使⽤的数据是上次迭代计算的结果,就产⽣了迭代间的相关性,可以拆分循环来减少相关。
向量化其他准则
  短⼩的循环更容易让编译器实现⾃动向量化; 避免在循环内使⽤break退出循环 避免在循环内使⽤过多的条件语句,减少可能产⽣的条件跳转; 让循环次数尽可能是2的幂次 让编译器知晓循环次数,减少对循环次数为0等的判断; 循环内调⽤的函数尽量inline内联 使⽤数组+索引的⽅式访问⽐指针形式
更容易向量化; 间接寻址(多重索引)不会向量化; 使⽤restrict关键字来告诉编译器没有重叠的内存区域;
总结
  本⽂介绍了ARM平台基于ARM v7-A架构的ARM Cortex-A系列处理器(Cortex-A5, Cortex-A7,Cortex-A8, Cortex-A9, Cortex-A15)上的NEON多媒体处理硬件加速器针对C/C++语⾔、汇编语⾔和NEON intrinsics如何编译和优化,包含如何向量化、向量化的ARMCC和GCC编译器选项、NEON的汇编和EABI程序调⽤规范、如何在bare-metal和Linux操作系统上检测NEON硬件、如何指导编译器进⾏向量化NEON指令的优化等内容。