sam_code@hotmail.com
之前在一些ARM CPU下,曾在编译时指定过Neon。
0. Neon简介:
0.1: 简介:
ARM Advanced
SIMD延伸集,(ARM
Cortex-A系列处理器的128位SIMD架构扩展)称为NEON技术,它是一个结合64bit和128bit的SIMD(Single
Instruction Multiple Data单指令多数据)指令集。其针对多媒体和讯号处理程式具备标准化加速的能力,NEON具有一组广泛的指令集、各自的寄存器阵列,以及独立执行的硬件。ARM
ENON技术可加速多媒体和信号处理算法(如视频编码/解码、2D/3D图形、游戏、音频和语音处理、图像处理技术、电话和声音合成)。
NEON的寄存器:有16个128位四字寄存器Q0-Q15,32个64位双字寄存器D0-D31,两个寄存器是重叠的,在使用的时候需要特别注意,不小心就会被覆盖掉。
0.2: SIMD:
通常我们进行多媒体处理的时候,很多的数据都是16位或者8位的,如果这些程序运行在32位的机器上,那么计算机有一部分的计算单元是没有工作的.所以这是一种浪费.为了更好的使用那些被浪费的资源.SIMD就应运而生了.SIMD这种技术就是使用一条指令,但对多个相同类型和尺寸的数据进行并行处理.就像我们现实生活中的好几个人都在做同一件事情那样,这样就可以将速度提升很多倍
0.3:使用Neon的方式:
按Sam的理解,使用Neon的方式有以下几种:
A:使用C的neon内联函数。
B:直接使用Neon汇编指令。
C:使用某些第三方库如OpenMAX.
各自的优缺点:
A:使用Neonintrinsics函数,可以在直接接触ASM的情况下,使用Neon。这些函数被定义在:
arm_neon.h
中。
类似于:
vadd_s8
(int8x8_t __a, int8x8_t __b)
此方法需要注意2点:
1.必须: #include
2.编译时必须加入; -mfloat-abi=softfp -mfpu=neon
B:使用汇编指令:
效率最高.使用intrinsics没法控制寄存器分配和内存对齐等。
1. Android NDK下
Neon的探索:
Sam着重研究的是使用intrinsics函数。研究标本为:/opt/android-ndk-r10b/samples/hello-neon/jni
为了不遗漏任何关键点,Sam从空文件main.cpp中开始写代码。
可以看到,代码基本全是从例子copy过来的。#include
必须要有。
main.cpp:
#include
#include
#include
#include
#include
#include
#include
#include
static void fir_filter_c(short *output, const short* input,
const short* kernel, int width, int kernelSize);
static double now_ms(void);
void fir_filter_neon_intrinsics(short *output, const short*
input, const short* kernel, int width, int kernelSize);
//#include
#include "cpu-features.h"
#define FIR_KERNEL_SIZE 32
#define FIR_OUTPUT_SIZE 2560
#define FIR_INPUT_SIZE (FIR_OUTPUT_SIZE + FIR_KERNEL_SIZE)
#define FIR_ITERATIONS 600
static const short
fir_kernel[FIR_KERNEL_SIZE] =
{
0x10, 0x20, 0x40, 0x70,
0x8c, 0xa2, 0xce, 0xf0, 0xe9, 0xce, 0xa2, 0x8c, 070, 0x40, 0x20,
0x10,
0x10, 0x20, 0x40, 0x70,
0x8c, 0xa2, 0xce, 0xf0, 0xe9, 0xce, 0xa2, 0x8c, 070, 0x40, 0x20,
0x10 };
static short fir_output[FIR_OUTPUT_SIZE];
static short fir_input_0[FIR_INPUT_SIZE];
static const short* fir_input = fir_input_0 +
(FIR_KERNEL_SIZE/2);
static short fir_output_expected[FIR_OUTPUT_SIZE];
static double now_ms(void)
{
struct timespec
res;
clock_gettime(CLOCK_REALTIME, &res);
return 1000.0*res.tv_sec
+ (double)res.tv_nsec/1e6;
}
static void fir_filter_c(short *output, const short* input,
const short* kernel, int width, int kernelSize)
{
int
offset = -kernelSize/2;
int
nn;
for (nn = 0; nn <
width; nn++) {
int sum = 0;
int mm;
for (mm = 0; mm < kernelSize; mm++) {
sum +=
kernel[mm]*input[nn+offset+mm];
}
output[nn] = (short)((sum + 0x8000) >>
16);
}
}
void fir_filter_neon_intrinsics(short *output, const short*
input, const short* kernel, int width, int kernelSize)
{
#if 1
int nn, offset =
-kernelSize/2;
for (nn = 0; nn <
width; nn++)
{
int mm, sum = 0;
int32x4_t sum_vec = vdupq_n_s32(0);
for(mm = 0; mm < kernelSize/4; mm++)
{
int16x4_t
kernel_vec = vld1_s16(kernel + mm*4);
int16x4_t
input_vec = vld1_s16(input +
(nn+offset+mm*4));
sum_vec =
vmlal_s16(sum_vec, kernel_vec, input_vec);
}
sum += vgetq_lane_s32(sum_vec, 0);
sum += vgetq_lane_s32(sum_vec, 1);
sum += vgetq_lane_s32(sum_vec, 2);
sum += vgetq_lane_s32(sum_vec, 3);
if(kernelSize & 3)
{
for(mm =
kernelSize - (kernelSize & 3); mm < kernelSize; mm++)
sum += kernel[mm] *
input[nn+offset+mm];
}
output[nn] = (short)((sum + 0x8000) >>
16);
}
#else
int nn, offset =
-kernelSize/2;
for (nn = 0; nn <
width; nn++) {
int sum = 0;
int mm;
for (mm = 0; mm < kernelSize; mm++) {
sum +=
kernel[mm]*input[nn+offset+mm];
}
output[n] = (short)((sum + 0x8000) >>
16);
}
#endif
}
int main(int argc, char** argv)
{
uint64_t features;
char buffer[512];
char tryNeon = 0;
double
t0, t1, time_c, time_neon;
if (android_getCpuFamily() != ANDROID_CPU_FAMILY_ARM) {
printf("\nCPU Not ARM\n");
return -1;
}
features = android_getCpuFeatures();
if ((features & ANDROID_CPU_ARM_FEATURE_ARMv7) == 0)
{
printf("\nNot an ARMv7 CPU !\n");
return -1;
}
if ((features &
ANDROID_CPU_ARM_FEATURE_NEON) == 0) {
printf("\nCPU doesn't support NEON !\n");
return -1;
}
printf("\nFeature: 0x%llx\n", features);
{
int nn;
for (nn = 0; nn < FIR_INPUT_SIZE; nn++)
{
fir_input_0[nn] = (5*nn) & 255;
}
fir_filter_c(fir_output_expected, fir_input,
fir_kernel, FIR_OUTPUT_SIZE, FIR_KERNEL_SIZE);
}
t0 = now_ms();
{
int count =
FIR_ITERATIONS;
for (; count > 0; count--) {
fir_filter_c(fir_output, fir_input, fir_kernel, FIR_OUTPUT_SIZE,
FIR_KERNEL_SIZE);
}
}
t1 = now_ms();
time_c = t1 - t0;
printf("\nFIR Filter benchmark:\nC version : %g ms\n", time_c);
printf("\n\nNeon version : ", sizeof
buffer);
t0 = now_ms();
{
int count =
FIR_ITERATIONS;
for (; count > 0; count--) {
fir_filter_neon_intrinsics(fir_output, fir_input, fir_kernel,
FIR_OUTPUT_SIZE, FIR_KERNEL_SIZE);
}
}
t1 = now_ms();
time_neon = t1 -
t0;
printf(" %g ms (x%g
faster)\n", time_neon, time_c / (time_neon < 1e-6 ? 1. :
time_neon));
return 0;
}
Android.mk:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_ARM_MODE := arm
LOCAL_MODULE := Test_Neon
LOCAL_STATIC_LIBRARIES := cpufeatures
LOCAL_SRC_FILES := main.cpp
LOCAL_CFLAGS := -DHAVE_NEON=1
-I/opt/android-ndk-r10b/sources/android/cpufeatures
-mfloat-abi=softfp -mfpu=neon
LOCAL_CXXFLAGS := -DHAVE_NEON=1
-I/opt/android-ndk-r10b/sources/android/cpufeatures
-mfloat-abi=softfp -mfpu=neon
LOCAL_LDLIBS :=
-L/opt/android-ndk-r10b/sources/android/libs/armeabi-v7a
-lcpufeatures
include $(BUILD_EXECUTABLE)
Application.mk:
# Build both ARMv5TE and ARMv7-A machine code.
APP_PLATFORM = android-8
APP_ABI := armeabi-v7a
#APP_ABI := $(ARM_ARCH)
#Sam modify it to release
APP_OPTIM := release
#APP_OPTIM := debug
#APP_OPTIM = $(MY_OPTIM)
APP_CPPFLAGS += -fexceptions
APP_CPPFLAGS += -frtti
#sam modify it from gnustl_static to gnustl_shared
#APP_STL := gnustl_static
#APP_STL := gnustl_shared
APP_STL := gnustl_shared
#APP_CPPFLAGS += -fno-rtti
#
APP_CPPFLAGS += -Dlinux -fsigned-char
APP_CFLAGS += -fsigned-char
#APP_CPPFLAGS += $(MY_CPPFLAGS) -Dlinux
#STLPORT_FORCE_REBUILD := true
编译过程会遇到以下几个问题:
1. android_getCpuFeatures系列函数未声明:
则加入#include
"cpu-features.h"
并在Android.mk中加入其路径:-I/opt/android-ndk-r10b/sources/android/cpufeatures
2. android_getCpuFeatures系列函数未定义:
进入:/opt/android-ndk-r10b/sources/android/cpufeatures
并编译之,Sam修改Android.mk, 把它编译成动态库。并保持Application.mk统一配置。
在K1平台上,运行结果是:
./Test_Neon
Feature: 0x7ff
FIR Filter benchmark:
C version : 117.865
ms
Neon version : 26.5964 ms
(x4.43163 faster)
显示对FIR Filter,使用Neon有4倍速度。
2.
如何判断当前平台是否支持Neon指令集:
2.1: 字符界面查看:
cat /proc/cpuinfo
在Features 项目中,看是否包含neon.
2.2:编程查看:
利用上一节中编译出的libcpufeatures.so 库得到CPU Features信息:
if (android_getCpuFamily() != ANDROID_CPU_FAMILY_ARM) {
printf("\nCPU Not ARM\n");
return -1;
}
features = android_getCpuFeatures();
if ((features & ANDROID_CPU_ARM_FEATURE_ARMv7) == 0)
{
printf("\nNot an ARMv7 CPU !\n");
return -1;
}
if ((features &
ANDROID_CPU_ARM_FEATURE_NEON) == 0) {
printf("\nCPU doesn't support NEON !\n");
return -1;
}
3. SIMD寄存器基本知识:
SIMD寄存器共有16个128位的向量寄存器构成。而32个双精度浮点寄存器共享了这16个寄存器;32个单精度浮点寄存器共享了前8个寄存器。因此,这寄存器组是与浮点寄存器共享的.
单精度寄存器用s0~s31表示;(32bit)
双精度寄存器用d0~d31来表示;(64bit)
而128位的SIMD寄存器则用q0~q15来表示.(128bit)
4. Neon的数据从内存向寄存器加载(vld)和从寄存器向内存存储(vst):
如何灵活有效的加载和存储数据,对SIMD来说非常重要。先举一下例子:
24-bit的RGB图像,像素在内存里的组织方式是R, G, B, R,
G,
B...,如果你想做一个简单的图像处理,比如把R和B通道互换,你该如何高效的使用NEON协处理器呢?
首先想到的办法是:从存储空间线性加载RGB数据到D寄存器(64位双精度寄存器),然后交换R和B数据。
但是这种线性加载的数据进行R和B通道的数据交换非常麻烦,需要首先掩码mask,然后移位并合并掩码数据。这种复杂的运算显然并不高效。如图所示:
从r0内存处,开始读取数据到寄存器。
D0,D1,D2这三个64bit寄存器中各防止了杂乱的8个色数据。这样非常不利于快速计算。
NEON提供了各种结构的加载和存储指令来处理这种情况,这些指令能把数据从存储区加载的同时还能把这些值分开存储到不同的寄存器中。
从r0内存处开始读取数据。
然后使用VLD3分开加载的数据就能方便的使用指令(VSWP d0,
d2)来进行R和B通道的交换了。
然后把结果再写入内存,当然也要使用interleave交织模式的存储,即VST3存储指令。
5. 结构化加载和存储语法和具体指令:
NEON结构化加载会读取内存内容到64-bit的NEON寄存器,使用可选的deinterleave选项,同样加载指令也可以采用这种reinterleave的方式把寄存器的内容写到内存空间。
NEON存储和加载的结构化方式,语法包括如下5个部分:
加载VLD或者存储VST指令助记符:instruction
mnemonic
一个表示interleave模式的数字,表示每个结构体元素间的间隔:interleave
pattern
表示每次访问单元的位宽比特数,即结构体内元素类型:element
type
读写的64-bit的NEON寄存器集合,最多可以列出4个寄存器,取决于interleave模式:NEON
registers
表示内存访问地址的ARM寄存器,该地址可以在每次访问时更新:
ARM address
register
交织模式:Interleave
Pattern:
加载和存储指令可以用从1到4个相同大小的元素的交织结构体,这些元素可以是NEON指令通常支持的8,16或者32比特。
VLD1是最简单的形式,从内存加载1~4个寄存器的数据,没有deinterleave,即线性加载;
VLD2加载2或者4个寄存器的数据,解交织奇偶元素到各自的寄存器,这样很容易的把交织的立体声音频数据分解为左右声道的数据;
VLD3加载3个寄存器的数据,很方便的把RGB的数据分为R、G、B通道;
VLD4加载4个寄存器的数据,解交织,用于分解ARGB图像数据;
存储和加载类似,只是把寄存器的数据interleave然后写到内存。
元素类型Element
Types
加载和存储interleave的数据的基本元素可以为8,16或者32比特的数据。比如NEON指令VLD2.16
{d0,
d1}将加载4个16-bit元素到第一个寄存器,然后4个16-bit元素到第二个寄存器,把临近的奇偶对分开保存到每个寄存器。
把元素大小变成32-bits还是加载相同大小的数据,但是只有2个元素来构成一个向量,同样分成奇偶元素部分。
6. 关于优化的几个例子:
#include // C version void add_int_c(int* dst, int* src1, int*
src2, int count) { int i; for (i = 0; i < count; i++) dst[i] =
src1[i] + src2[i]; } } // NEON version void add_float_neon1(int*
dst, int* src1, int* src2, int count) {
int i; for (i = 0; i < count; i += 4) { int32x4_t in1, in2, out;
in1 = vld1q_s32(src1); src1 += 4; in2 = vld1q_s32(src2); src2 += 4;
out = vaddq_s32(in1, in2); vst1q_s32(dst, out); dst += 4; }
}
vld1q_s32(src1):
被解析为:
vld1.32 {d0, d1}, [r0]
vld:
load数据到寄存器。 1:表示依次读取。32:每份数据32bit. d0,
d1,两个64bit寄存器。所以,表示要读4份数据。
r0:从r0内存处开始读。