http://www.music-video.cn/2017/12/20/%E9%99%84%E5%BD%95a%EF%BC%9A%E9%80%9A%E8%BF%87ne10%E7%9A%84%E4%BA%A4%E5%8F%89%E7%BC%96%E8%AF%91%E8%BE%93%E5%85%A5%E7%90%86%E8%A7%A3ndk-build%E6%A0%B7%E7%AB%A0/
目前大部分智能手机的已经配备了高清摄像头、高保真麦克风,由此而带来的声音类应用于图像类应用越来越多,而本书所讲解的就是基于移动平台的音视频类应用的开发。但是在处理这一些音视频数据的时候,单纯依靠现有的CPU的计算能力其实是远远不够的,所以对于一个音视频引用来讲,提高性能是永无止境的。在视频处理方面,更多的会使用OpenGL ES技术,利用显卡的并行计算来提高处理图像或视频的速度,但是在音频处理方面却很少有比较成熟的技术以供开发者使用。ARM NEON技术采用SIMD(单指令,多数据)体系结构,可以有效提升多媒体和信号处理应用程序的性能,从而增强用户体验。同时,NEON技术与ARM处理器紧密结合,提供单指令流和内存的统一视图,从而能够提供一个具有更简单工具流的开发平台。由于目前市面上的Android设备和iOS设备都使用的是ARM架构的芯片,所以在音视频处理的过程中使用ARM提供的NEON指令集来加速我们的运算是一件非常有意义的事情。但是很多基础的Math功能以及信号处理的FFT、FIR、IIR等滤波器让开发者从头开始写,也基本上是不现实的事情。所以我们要讲解的Ne10应运而生。
Ne10是由ARM主导开发的一个开源软件库。该库旨在提供一系列通用的,基于ARM NEON架构并且经过深度优化的函数集合。通过调用该库函数可以让软件开发人员免于编写重复的底层汇编代码,同时也能充分利用ARM NEON SIMD指令的并行运算能力。Ne10的主要目录结构包括:文档(doc)、头文件目录(inc)、samples(示例目录)、android(安卓平台下的动态库)、common(基础库)和modules(模块目录)。部分目录会在后续的章节中逐一进行介绍,而这里我们来看一下modules目录下的结构,在modules目录下有三个目录,功能如下:
Ne10的编译是基于cmake进行编译的,首先开发者需要开发环境中安装cmake,注意一定要安装命令行方式的cmake,不要安装一些图形化界面的。cmake的全称是cross platform make,从它的全称上其实可以看出来它是一个跨平台的编译工具。由于各个平台的编译环境与构建语法不同,这对于开发者开发一款跨平台的公共库或者软件的时候是一件很困难的事情。而cmake可以用统一的语法来编译出多个平台的makefile文件或project文件,然后执行make命令就可以编译出目标包了。 如何安装cmake呢?在MacOSX系统下开发者直接使用brew进行安装cmake就可以了:
brew install cmake
安装好了cmake之后,接下来就可以编译Ne10这个库了。首先进入Ne10的根目录,然后进入doc目录,打开文件building.md,在文件building.md中可以找到对于编译Android平台的帮助。可以看到编译步骤如下:
a:建立build目录,并且进入到这个目录
mkdir build && cd build
b:将NDK目录配置到ANDROID_NDK变量中
export ANDROID_NDK=/absolute/path/of/android-ndk
c:指定编译的目标平台是armv7,当然也可以指定为aarch64
export NE10_ANDROID_TARGET_ARCH=armv7 #Can Also be “aarch64”
d:指定cmake的的配置文件路径,使用cmake来生成对应平台的makefile文件
cmake DCMAKE_TOOLCHAIN_FILE=../android/android_config.cmake ..
f:执行make指令,编译出对应平台下的包
make
make命令运行完毕之后,如果没出现错误日志就代表Ne10编译成功了。
开发者可以进入到build目录下,这个目录就是编译的目标目录了。这个目录下有一个android目录,这个android目录下有一个NE10Demo,就是运行在Android设备上的应用程序。开发者可以将NE10Demo中的jni目录下面的libNE10_test_demo.so拿出来,放到工程下面的libs中的armeabi-v7a目录下面,然后将工程导入到IDE中,运行起来。
官方的这个Demo工程是使用一个WebView来展示界面的,我们先不管界面的展示,在jni里面的源码中可以看到,NE10会利用自己的单元测试用例来测试NE10的性能,并且与CPU的运行的速度做对比。我们可以在Java层代码中打一个断点来看底层返回的string类型的结果,默认的测试函数是abs这个函数,测试结果如下:
{ “name” : “test_abs_case0 1”, “time_c” : 11441, “time_neon” : 1448 },
{ “name” : “test_abs_case0 2”, “time_c” : 8289, “time_neon” : 1858 },
{ “name” : “test_abs_case0 3”, “time_c” : 11193, “time_neon” : 2781 },
{ “name” : “test_abs_case0 4”, “time_c” : 13575, “time_neon” : 2229 }
可以看到abs这个函数利用neon加速之后的结果是纯使用CPU计算的速度的5倍以上。如果想得到更多其他函数的测试结果,读者可以自己去改动jni层的测试用例调用,然后在进行使用cmake以及make指令编译出so包,然后运行就可以得到结果了。
运行成功了Ne10的官方Demo之后,如果开发者想开发基于Ne10的应用该如何做呢?依据之前的开发经验,其实最简单的方式就是拿到include文件与静态库文件,然后放到我们的Native代码中,然后分别在编译和链接阶段能找到这两部分文件就可以了。具体做法如下:
还是先进入到build目录中,找到modules目录下面的libNE10.a,这就是我们朝思暮想的静态库文件。那头文件在什么位置呢?其实就在主目录下面的inc(include)目录,这个目录里面就是编译阶段需要的头文件。按照我们之前的目录构建方式,那我们就将头文件和静态库文件放到我们的Native代码中,然后开始开发基于NE10的应用就好了,
笔者和Ne10的作者曾经在github上对Ne10在Android平台的编译与使用有过比较多的交流,Ne10的作者之一的joe savage是一个非常nice的人,经过多次讨论,笔者发现了Ne10更多的内部细节。按照1-2-2小节中的步骤将NE10编译成功之后,进入到build目录,可以看到这个目录下面看到三个比较重要的目录,分别是android目录、samples目录和modules目录,接下来我们重点介绍一下这三个目录。
android目录
我们一块来看到build目录中的android目录,这个目录下面是编译好的安卓平台下面的动态so库,以及编译过程中cmake产生的中间文件。按照1-2-3小节中的描述,将动态库libNe10_test_demo.so放到官方Demo工程中运行,可以得到对比测试的结果。这个动态库的源码内容实际上是Ne10根目录中的android目录下面的jni中的代码,而jni里面的代码使用的却是Ne10根目录下的test目录下的测试case。大家可以想一下,这个编译动态库的过程其实和绝大多数的安卓工程编译动态库都不一样。因为大部分的开发者正常编译一个安卓工程的动态库使用的是ndk-build命令。
但是Ne10的构建方式可不是使用ndk-build这个脚本,而是使用了更加通用的cmake,cmake是一个跨平台的构建工具,用自己的描述语言或者语法可以生成对应平台的Makefile(make脚本文件)文件。开发者并不值得在安卓平台的每个项目都这样做,因为Ne10这个项目本身就不仅仅是安卓平台的项目,而是所有使用ARM架构的系统都可以使用的开源库。所以不同应用场景自然选择不同的解决方案,显然使用cmake对于NE10是最佳的解决方案,而对于Android工程的Native代码的构建场景,使用ndk-build其实才是最佳的解决方案。如果开发者想更改NE10Demo的测试程序,进行一些自己集成NE10之后的性能测试或者正确性测试(当然Ne10作者都已经做过了这一些测试,但是由于构建方式的不同以及开发者的工程可能会有比较复杂的业务逻辑,所以自己做测试也在所难免),开发者就可以修改Ne10项目目录中的android目录下面NE10Demo中的jni目录下的源码文件NE10_test_demo.c,接下来再到build目录下面执行make命令,最后再把so文件拷贝到对应目录去编译并运行安卓程序,当然如果增加jni函数的话也要对应的在java文件中增加native方法的声明。以上就介绍完毕了andorid目录下的结构,其实这个目录中主要就是针对于Android工程编译的动态so库,并且开发者可以去更改对应的源码文件然后使用这个so库在进行测试。
NDK-BUILD介绍
ndk-build命令是一个脚本,在指定的NDK-ROOT下面。而ndk-build这个脚本实际上会检查工程当前的Application.mk文件里面的配置选项,包括交叉编译的gcc的版本、APP-CFLAGS和APP-CPPFLAGS,以及是否开启异常,Rtti等参数。当然如果没有Application.mk这个配置文件,ndk-build指令会使用一系列默认的配置。其实最重要的就是要确定使用的gcc版本以及要编译的目标平台,例如我们使用gcc4.8编译armv7-a平台上的包。然后ndk-build才会读取Android.mk里面的配置,包括CFLAGS、LDFLAGS、源码文件以及include的预编译mk,最终进行编译和链接。而无论Application.mk还是Android.mk配置文件中指定的CFLAGS、LDFLAGS这一些参数的默认值都在NDK目录下的哪一个文件中指定的呢?其实它们都存在与NDK目录中的toolchains目录下对应的gcc版本的目录中的setup.mk这个文件中,例如使用的gcc版本为4.9的时候,setup.mk所在的目录为:
$NDK_ROOT/build/core/toolchains/arm-linux-androideabi-4.9或者
$NDK_ROOT/toolchains/arm-linux-androideabi-4.9
由于不同的NDK版本所在的位置不同,最终当ndk-build脚本被执行的时候,可以根据默认配置以及Android.mk里面的配置构建出对应的动态库或者静态库或者二进制的可执行程序。
2. samples目录
我们一块来看到build目录中的samples目录,这个目录里面可以看到一个二进制的可执行文件NE10_samples_static,我们可以将这个文件放到Android手机上,以命令行工具的方式运行它。首先找一台root了的手机(因为只有获得root权限,才能方便直接登入系统去执行这个命令),然后利用adb push命令将这个二进制文件推送到sdcard上:
adb push NE10_samples_static /mnt/sdcard/NE10_samples_static
接着使用adb shell命令登入这台安卓设备:
adb shell
然后将上一步从电脑推进来的二进制程序拷贝到/data/目录下面,并且增加执行权限:
su
cp /mnt/sdcard/NE10_samples_static /data/
cd /data/
chmod 777 NE10_samples_static
最后运行这个二进制命令:
./NE10_test_static
如果没有错误的话,应该可以看到官方二进制Demo的测试结果。开发者可以修改对应的源码文件,然后执行make命令,生成新的二进制文件,将二进制文件放入Andorid设备中,然后执行来做一些快速的测试。那如何修改二进制命令对应的源码文件呢?首先切换到Ne10的根目录下,然后进入samples目录,接下来就可以修改对应的主文件或者单元测试的文件了,等修改完毕之后,再回到build目录进行make,然后找到最新的二进制可执行程序推送到安卓系统内部,再一次执行可以看到修改后的效果了。
其实在平时的开发过程中,开发者并不是总需要安卓的开发环境才能测试Native层的代码,其实也可以编译二进制的可执行文件来做快速测试。秘诀就在于Android.mk配置文件的最后一行,如果include的是shared library就是构建成动态库,而include的是static library就是构建静态库,但是如果我们include的是execute library那就是构建二进制可执行文件。而include 的任何一个变量,其实都是一个预定义的文件存在于NDK-ROOT目录下,路径为:
$NDK_ROOT/build/core
所以如果想用ndk-build编译出来一个二进制程序的话,那就在Android.mk文件的最后一行include那个execute library就可以了。
3.modules目录
我们再来看一下build目录下面的modules目录,这个目录中有一个静态库libNE10.a,这个文件就是编译好的armv7-a平台下的静态库。开发者可以将这个静态库作为一个prebuilt的静态库链接到自己的程序中,然后直接执行ndk-build命令,就可以编译出动态so库了。但是这个地方有可能会出现一个编译失败的问题,错误如下:
ld: error: jni/prebuilt/libNE10.a(NE10_fft_generic_float32.c.o) uses
VFP register arguments, output does not
其实根本原因在于Ne10默认的编译选项里面开启的是硬浮点运算,即:
-mfloat-abi=hard -mfpu=vfp3
但是ndk-build这个脚本使用是gcc版本对应的setup.mk文件里面的编译选项和链接选项,而setup.mk文件中默认的选项确实使用的是如下指令:
-mfloat-abi=softfp -mfpu=vfpv3-d16
所以使用默认的编译选项肯定是不对的,所以需要在Application.mk里面重新指定编译选项和链接选项来覆盖掉默认的选项配置:
APP_CPPFLAGS := -pie -mthumb-interwork -mthumb -march=armv7-a -mfloat-abi=hard
-mfpu=vfp3 -Wl,–no-warn-mismatch -std=gnu99 -fPIC
APP_CFLAGS := -pie -mthumb-interwork -mthumb -march=armv7-a -mfloat-abi=hard
–mfpu=vfp3 -Wl,–no-warn-mismatch -std=gnu99 -fPIC
APP_LDFLAGS := -mthumb-interwork -mthumb -march=armv7-a -mfloat-abi=hard
–mfpu=vfp3 -Wl,–no-warn-mismatch -std=gnu99 -fPIC
而具体的使用ndk-build脚本编译Native代码的时候,所使用的编译选项和链接选项如何查看呢?开发镇仅需要在使用ndk-build的时候在后边加上V=1就可以看到编译选项和链接选项了,如下:
ndk-build V=1
执行这个命令之后,可以查看编译选项和链接选项是否是我们更改之后的选项。
4.软浮点和硬浮点
在上一小节中我们曾说过Ne10的编译是使用cmake工具来进行编译的,所以我们必须要正确的安装cmake,当然我们使用brew来进行安装十分方便。而在Ne10的默认编译选项里面包含了对浮点数运算的选项配置:
-mfloat-abi=hard -mfpu=vfp3
这个编译选项是什么意思呢?其实是这样子的,在gcc的编译选项-mfloat-abi参数可选值有三个,分别是soft、softfp和hard,解释如下:
需要注意的是在兼容性方面,soft模式与后两者是兼容的,但softfp和hard两种模式是不兼容的。默认情况下在Application.mk里面会有如下配置:
APP_ABI := armeabi-v7a
NDK_TOOLCHAIN_VERSION = 4.9
配置会使用NDK中4.9版本的gcc编译armv7-a平台下的包,然后直接使用Ne10的静态库进行链接的时候,在链接阶段就会出现错误了:
ld: error: jni/prebuilt/libNE10.a(NE10_fft_generic_float32.c.o) uses
VFP register arguments, output does not
这其实就是libNE10.a这个静态库在编译阶段使用的编译选项中开启了硬浮点运算,而现在使用ndk-build脚本进行构建的时候使用的是确是软浮点,链接遇到了不同的输入文件类型就报出了上述错误,要想解决这个问题我们可以将APP_ABI配置成为armeabi-v7a-hard:
APP_ABI := armeabi-v7a-hard
这样gcc在寻找编译选项(setup.mk)的时候就会寻找硬浮点了,setup.mk文件中配置如下:
ifeq ($(TARGET_ARCH_ABI),armeabi-v7a)
TARGET_CFLAGS += -mfloat-abi=softfp
else
TARGET_CFLAGS += -mhard-float \
-D_NDK_MATH_NO_SOFTFP=1
TARGET_LDFLAGS += -Wl,–no-warn-mismatch \
-lm_hard
endif
但是NDK在最新版本中已经不支持armeabi-v7a-hard的模式了,所以我们需要配置编译选型和链接选项,配置编译选项里面的将浮点预算使用硬浮点的fpu是为了保证正确性,链接选项里面的-Wl,–no-warn-mismatch是为了告诉链接器忽略警告保证链接成功,不要在检查输入文件的格式不同。所以最终Application.mk如下:
APP_ABI := armeabi-v7a
APP_CPPFLAGS := -pie -mthumb-interwork -mthumb -march=armv7-a -mfloat-abi=hard
-mfpu=vfp3 -Wl,–no-warn-mismatch -std=gnu99 -fPIC
APP_CFLAGS := -pie -mthumb-interwork -mthumb -march=armv7-a -mfloat-abi=hard
–mfpu=vfp3 -Wl,–no-warn-mismatch -std=gnu99 -fPIC
APP_LDFLAGS := -mthumb-interwork -mthumb -march=armv7-a -mfloat-abi=hard
–mfpu=vfp3 -Wl,–no-warn-mismatch -std=gnu99 -fPIC
NDK_TOOLCHAIN_VERSION = 4.9
然后在运行ndk-build脚本,等执行完毕没有错误的话,就完全编译好了这个动态库。有的读者可能会问了,是如何找到这一些参数的呢?其实在1、2、3小节中所讲解的三个目录下都有一个CMakeFiles目录,每个目录下面会有一个XXXXX.dir目录,这个目录里面有一个flags.make以及link.txt,这两个文件中就是编译和链接命令以及带的参数。其实cmake是根据自己的脚本文件中的配置生成了这一些参数,不过在这里找到的编译选项以及链接选项才是最终的选项,就像我们使用ndk-build脚本来编译的话,我们写上V=1才可以看到中间过程中编译和链接的选项。
其实还有一种方法就是将Ne10的编译选项配置一下,使用软浮点,就需要我们在执行cmake的时候带上参数,即:
cmake -DNE10_ARM_HARD_FLOAT=OFF
-DCMAKE_TOOLCHAIN_FILE = ../android/android_config.cmake ..
这时候编译出来的静态库就是软浮点运算的了,但是Ne10的作者不推荐这样做,因为这样一方面效率会低一点,另外一方面有一些汇编文件里面的代码可能依赖于硬浮点的运算,这有可能会造成错误。
1、浮点数组加(减乘除)一个float常量数值放入目标浮点数组中
2、向量数组(2D、3D、4D)加(减乘除)一个向量常量放入目标向量数组中
3、浮点数组加(减乘除)另外一个浮点数组(按照相同的index进行运算)放到目标浮点数组中
4、向量数组(2D、3D、4D)加(减乘除)另外一个向量数组(按照相同index进行运算)放到目标向量数组中
5、矩阵(二维、三维、四维)与矩阵的加减乘除
6、矩阵与向量相乘
7、浮点数组都设置为一个常量
8、浮点数组绝对值
9、一个常量减去一个浮点数组中的每一个元素放置到目标浮点数组中
10、向量常量减去一个向量数组中的每一个原宿放置到目标向量数组中
11、浮点数组乘以常量在加上另外一个浮点数组(按照index进行)放入目标浮点数组中
12、向量数组乘以向量常量再加上另外一个向量数组(按照index进行)放入目标向量数组中
13、浮点数组乘以一个另外一个浮点数组(按照index进行相乘)再加上一个浮点常量放入目标浮点数组中
14、向量数组乘以另外一个向量数组(按照index进行相乘)再加上一个向量常量放入目标向量数组中
15、矩阵的转置、单位矩阵运算等数学运算
在不同的Android手机上做测试,FFT的结果要快3倍左右,有的甚至能到5倍,所以效果还是很可观的。测试样本是时间长度为10s ,采样频率为44100,双声道的的PCM,先做FFT比较结果是否与MayerFFT一致(平方后相加进行浮点数比较,相差在0.0001以内),然后在做逆FFT在听声音是否正常,如果都没有问题就代表结果是正确的。最终结果的正确性与性能对比结果如下:
机型 | 正确性 | 速度(DSP/CPU) |
Nexus 5X | 正确 | 3.x倍 |
Oppo R9 | 正确 | 3.x倍 |
三星Note4 | 正确 | 3.x倍 |
VIVO X7 | 正确 | 2.x倍 |
红米Note3 | 正确 | 3.x倍 |
三星Note2 | 正确 | 2.x倍 |
红米 | 正确 | 2倍左右 |
小辣椒 | 正确 | 2.x倍 |
魅蓝2 | 正确 | 3倍左右 |
小米3 | 正确 | 3.x倍 |
Oppo R9 Plus | 正确 | 2.x倍 |
Lenovo K8 | 正确 | 2.x倍 |
小米5 | 正确 | 3.x |
三星S6 | 正确 | 3倍左右 |
坚果手机 | 正确 | 3倍左右 |
小米4 | 正确 | 4倍左右 |
虽然Ne10大多数可以达到3倍,但是提升也是不少,可以认为利用Ne10来给我们来做neon加速,速度和正确性是没有问题的。
Ne10 ndk-build Native编译 Android.mk