一、背景简介
ARM CPU 最初只有普通的寄存器,可以进行基本数据类型的基础运算。从 ARMv5 架构开始引入 VFP(vector-floating-point) 指令扩展,可以通过使用短向量指令来加速浮点计算。从 ARMv7 架构开始引入 NEON 技术,NEON 技术同样是依靠向量指令来加速计算。鉴于 NEON 技术提供的向量技术加速效果体验更优秀,从 ARMv7 架构开始使用 VFP 向量指令加速的模式被弃用,因此 VFP 单元有时也称之为 FPU(Floating Point Unit)单元。
NEON 技术意在通过加速音频和视频编解码、用户界面、2D/3D图形和游戏来改善多媒体用户体验。NEON 还可以加速信号处理算法,以加快音频和视频处理、声音和面部识别、计算机视觉和深度学习等应用。
二、基本架构
ARM NEON 技术本质上是一种高级的单指令多数据(SIMD)架构扩展,这种扩展仅在一些 ARMv7-A 和 ARMv7-R 架构以及 ARMv8 架构上支持。
熟悉 ARMv7-A 架构的应该知道 ARMv7 架构的内核是一个32位的系统,使用32位的寄存器。但是 NEON 单元使用的是64位或者128位的寄存器。这里的原因就是 NEON 单元使用独立的寄存器文件。不过,NEON 单元还是完全集成到处理器中的,可以和处理器共享整型操作单元、循环控制和缓存资源,相比于使用硬件加速器,大大降低了面积和功耗成本。而且由于 NEON 单元和应用程序使用相同的地址空间,可以使用更简单的编程模型。
ARM NEON 技术的核心是 NEON 单元,主要由四个模块组成,分别是 NEON 寄存器文件、整型执行流水线、单精度浮点执行流水线和数据加载和重排流水线。
NEON 单元
三、NEON 寄存器
NEON 寄存器主要是用来存放包含相同数据类型元素的向量。在 ARMv7 架构中, 一共有16个128位寄存器,这个128位寄存器也称之为 Q 寄存器,一个128位寄存器又可以分为两个64位寄存器,即一共有32个64位寄存器,64位寄存器又称之为 D 寄存器。在ARMv8 架构中寄存器的数量相比 ARMv7 架构数量翻倍。Q 寄存器和 D 寄存器对应表如下所示:
关系对应表
NEON 向量的元素数量取决于寄存器的类型和元素的数据类型。NEON 寄存器支持常见的数据类,包括整型和浮点类型等,具体如下所示:
NEON 寄存器元素类型
根据向量元素的数据类型以及寄存器的类型,向量的类型如下所示:
NEON 寄存器向量类型
从上表中可以看出,对于32位数据,比如 int 和 float 类型数据,一个 Q 寄存器包含四个元素,D 寄存器则包含两个元素。对于16位数据,比如 float16 和 short 类型,一个 Q 寄存器则包含八个元素, D 寄存器则包含四个元素。对于8位数据, 比如 char 和 poly 类型数据,一个 Q 寄存器包含十六个元素,D 寄存器则包含八个元素。
四、NEON 调用
ARM 平台提供了四种使用 NEON 技术的方式,分别为 NEON 内嵌函数、NEON 开源库、编译器自动向量化和 NEON 汇编。
调用方式
NEON 内嵌函数调用类似于普通函数调用,通过调用函数接口告知编译器需要优化的代码,编译器在编译阶段直接使用 NEON指令替换这些内嵌函数而不是执行类似子函数调用的操作。NEON 内嵌函数提供了一种低级的 NEON 指令访问方式,而编译器负责将 NEON 指令替换成汇编语言的复杂任务,主要包括寄存器分配和代码调度以及指令集重排,来达到获取最高性能的目标。NEON 内嵌函数的缺点在于汇编语言和开发代码不一致。
鉴于 NEON 指令的强大优化效果,市场上出现了很多支持 NEON 优化的开源库,比如 Ne10、OpenMAX、ffmpeg、Eigen3和Math-neon等。
矢量化编译器可以将 C 或 C++ 源代码进行矢量化,以实现对 NEON 硬件的有效使用。这意味着可以编写可移植的 C 代码,同时还可以获得 NEON 指令带来的性能水平。
当使用的是 ARM 编译器工具链的时候,可通过下图几种方式实现自动向量化:
自动向量化方式
当使用的是 GCC 编译器工具链的时候,可通过下图几种方式实现自动向量化:
使用 GCC 时需要指定 CPU,否则会使用编译环境默认的内核,导致代码无优化或者异常。如果开启 -O3 优化选项,则默认开启 -ftree-vectorize 选项。
如果想获取非常高的性能提升,手写 NEON 汇编优化代码是最有效的方法,GNU 汇编器 (gas) 和 ARM 编译器工具链汇编器 (armasm) 都支持 NEON 指令的汇编。ARM 嵌入式应用程序二进制接口 (EABI) 规定了哪些寄存器用于传递参数、返回结果或必须保留。
五、NEON 指令集
鉴于 NEON 指令集是处理向量类型的数据,所以这里先给出 NEON 指令集支持的向量类型,具体如下表所示:
NEON 向量类型
从上表可以看成,NEON 指令集的向量是由基本上就是常见的数据类型组成,根据 D 寄存器和 Q 寄存器以及数据类型形成多种向量类型。这些向量类型将作为 NEON 指令集的输入输出参数参与计算。
NEON 指令集提供了多种多样的功能,基本上可以满足绝大部分代码的使用需求,具体支持的功能如下图所示:
NEON 指令表
从上表中可以看出来 NEON 指令集支持的功能有加减乘法、特殊相邻元素加法、饱和乘法、乘累加、移位、逻辑运算、极值获取、BIT统计等。除此之外,还支持比较大小、取倒数、向量分割拼接重排、查表等功能。
此外,按照 NEON 指令的输出输入向量类型,还可以将 NEON 指令分为以下几种:
NEON 指令类型
常指令,结果向量和操作数向量的长度、元素类型一致。下图展示了以下指令的过程:
VADD.I16 Q0, Q1, Q2
长指令通常对双字向量进行操作,并产生一个四字向量。结果元素的宽度是操作元素的两倍。长指令是用指令后面的 L 来指定的。下图显示了一个长指令的例子,输入操作数在操作前被提升为四字。
窄指令对四字向量操作数进行操作,并产生一个双字向量。结果元素是操作数元素宽度的一半。窄指令是用指令后面的 N 来指定的。如下图所示。
宽指令对一个双字向量操作数和一个四字向量操作数进行操作,产生一个四字向量结果。结果元素和第一操作数的宽度是第二操作数元素的两倍。宽指令有一个 W 附加在指令上。下图显示了这一点,输入的双字操作数在操作前被提升为四字。
六、优化示例
在图像处理时,彩色图转灰度图是很常见的一种图像格式转换。彩色图转灰度图的公式如下所示:
根据上述公式,实现彩色图转灰度图的 C 语言代码和 NEON 优化代码。代码如下所示:
static void BGR2GRAY(const unsigned char *bgr, int width, int height, unsigned char *gray) {
int coef_q8[3] = { 29, 150, 77 };
unsigned char *temp_s = (unsigned char*)bgr;
unsigned char *temp_d = gray;
#ifdef USE_NEON
uint8x8_t vwr = vdup_n_u8(coef_q8[0]);
uint8x8_t vwg = vdup_n_u8(coef_q8[1]);
uint8x8_t vwb = vdup_n_u8(coef_q8[2]);
#endif
for (int row = 0; row < height; row++) {
int col = 0;
#ifdef USE_NEON
for ( ; col < width - 8; col += 8) {
uint8x8x3_t vsrc = vld3_u8(temp_s);
uint16x8_t vsum = vmull_u8(vsrc.val[0], vwr);
vsum = vmlal_u8(vsum, vsrc.val[1], vwg);
vsum = vmlal_u8(vsum, vsrc.val[2], vwb);
vst1_u8(temp_d, vshrn_n_u16(vsum, 8));
temp_d += 8;
temp_s += 24;
}
#endif
for ( ; col < width; col++) {
temp_d[0] = (temp_s[0] * coef_q8[0] + temp_s[1] * coef_q8[1] + temp_s[2] * coef_q8[2]) >> 8;
temp_d += 1;
temp_s += 3;
}
}
return;
}
七、结语
NEON 技术所能探讨的内容远不止于此,这里仅仅介绍了 NEON 技术最基础的知识,更多是概括性介绍,后续将对每个点进行深度分析探讨。
八、附录
以下是部分官方文档地址链接