65.9K
CodeProject 正在变化。 阅读更多。
Home

优化 x86 上 Android 应用的技巧和窍门

2014 年 11 月 17 日

CPOL

7分钟阅读

viewsIcon

9578

本文将重点介绍优化基于 NDK 的应用程序。这些应用程序可能仅包含 C/C++ 代码,也可能包含第三方库和/或汇编代码。

引言

英特尔对帮助开发者提供能在英特尔架构上良好运行(甚至最优运行)的 Android 应用程序有着既得利益。英特尔在社区层面努力优化 Dalvik Java、V8 引擎和 Bionic C,贡献代码库,并发布包含 32 位和 64 位 IA 内核的版本;同时,他们也在创建新的工具来帮助 Android 开发者。其中许多工具专注于提高性能,使其超越 x86 的默认 ARM 翻译层(libhoudini)所能提供的性能。

但首先,选择正确的工具。创建 Android 应用有 3 种常用方法。

  1. 使用 Android SDK API 编译的 Java,在 Dalvik VM 中运行。注意:关于 ART 的文章即将推出,适用于 Android L。
    使用最新的 SDK 可以解决大部分差异,尽管您可能仍需关注为高分辨率屏幕分配的内存。最重要的是,使用 Intel® HAXM 加速您的 Android 模拟软件,可以更快地进行测试(需要 Intel® 虚拟化技术和 XD,均设置为 ON)。
  2. 专注于 Web 的 HTML5JavaScript。有关开源 Android 的信息,请查看 Android-IA 网站
  3. NDK 创建或移植(用 C++ 编写)。如果您有处理器密集型功能或已有 C++ 代码,这是首选方法。原生 C++ 代码通常(但不总是)运行得更快,因为它直接与硬件对话,代码在执行前被编译成二进制文件,无需解释为机器语言。

本文将重点介绍优化基于 NDK 的应用程序。这些应用程序可能仅包含 C/C++ 代码,也可能包含第三方库和/或汇编代码。

注意:如果您还没有 Android 开发环境(IDE),新的工具套件 Intel® INDE(Intel® 集成原生开发者体验)将加载选定的 Android IDE,并下载和安装多个英特尔工具,帮助您创建、编译、调试和发布 Android 应用程序。请点击这些链接,了解关于注册和 安装 Intel® INDE 以及 使用 Eclipse* IDE 设置它 的系列文章,其中包含关于设置 NDK 和 SDK*、Eclipse*,以及在模拟器(包括如何加速它)或基于英特尔® 架构的设备上运行的视频链接。

总的来说,NDK 开发涉及以下步骤以及最小的更改,以适应 x86 架构。

  1. 创建 Android 项目和 jni 文件夹。编辑 Application.mk,将其设置为 APP_ABI = all(如果文件大小允许 ARM* 和 x86 包含在同一包中)或 x86。注意:APP_ABI 设置还会影响浮点运算 - 见下文。
  2. 代码。任何原生(C++)代码都可以重用。重写任何内联汇编代码或 ARM 特定代码。使用 javah 创建 JNI/原生代码头文件。确保使用 JNIEXPORT 和 JNICALL 宏来解释 Windows 标准 C++ 约定与 Java/JNI 之间的差异。
  3. 编译/构建库(调用生成 .so 库并将其放入相应的项目目录)。使用“ndk-build APP_ABI = X86”并进行一些构建标志更改 - 见下文。另外,请务必重新编译任何第三方库。
  4. 从 Java 调用。在 Java 中声明原生(C++)函数调用,并使用 System.loadlibrary() 加载共享库。
  5. 调试。可以通过运行 ndk-build 并将清单设置为可调试来使用 ndk-gdb 进行调试。确保 adb 目录已添加到 PATH,并且只有一个目标正在运行。

除了基本的“移植”之外,还有一些优化可用。

优化技巧

  1. 使用 Intel® HAXM 进行硬件辅助模拟,以加快基于软件的 Android 模拟器。Intel® HAXM 需要 Intel® 虚拟化技术(Intel® VT)和 XD 设置为 ON。
  2. 设置 APP_ABI = x86(创建一个包含所有二进制文件的 apk)或 = armeabi armeabi-v7a x86,具体取决于您的文件大小限制。(注意 x86 包含硬件浮点,armeabi-v7a-x86 在一定程度上也包含)。
  3. 编译时,使用 gcc “-malign-double”。(这是为了内存对齐 - 另见 #9)
  4. 编译时,添加适当的 CPU 线程标志。
    对于 Intel® Atom™ 处理器的超线程功能,尝试使用 -mtune=atom -mssse3 -mfpmath=sse。
    对于非超线程(BYT, SLM, Merrifield),使用 -mtune=slm -msse4.2 -mfpmath=sse。
    使用 -march= 限制到指定的 CPU(mtune 运行在更多型号上,但为列出的类型进行优化)。
    Atom 上 -mavx 尚无用处。
  5. 使用小端字节序(NDK 默认为此)。ARM* 支持大端和小端,Intel® Atom™ 仅支持小端,因此请检查您的 gcc 标志。
  6. 使用 gcc v4.8。注意两个工具链路径(android-ndk\toolchains\arm-linux-androideabi-4.8 vs. x86 android-ndk\toolchains\x86-4.8)。
  7. 请务必使用正确的 JNIEXPORT 方法签名来设置进入原生代码的入口点 - (匹配头文件的函数签名,以确保源代码在 Windows* 上可以编译)。
    JNIEXPORT void JNICALL Java_ClassName_MethodName
  8. 编译后,检查系统日志,确保目标原生库在运行时成功加载。(日志中会显示“added shared lib //<path>”)。
  9. 显式强制内存对齐以防止加载错误和网络数据包问题。ARM 占用 24 字节,但需要 8 字节对齐才能存储 64 位变量,而 x86 占用 16 字节。所以尽量确保数据结构为 16 字节对齐。然后,在将数据从该结构加载到 XMM 寄存器时,使用对齐的移动(MOVAPS, MOVNTA)。请参阅 《减少内存访问未对齐的影响》
  10. 将数据直接写入主内存(流式存储指令 MOVNTPS, MOVNTQ),因为 Intel® Atom™ 处理器没有 L3 缓存。这还可以通过避免缓存逐出时的脏写回,来节省带宽消耗。
  11. 避免由于 L2 缓存有限而导致的停顿。除了特定情况(例如,数据加载和存储到同一地址、相同大小的操作数,并且从通用寄存器完成)外,Intel® Atom™ 处理器在写入缓存时,加载会停顿几个周期。此外,SSE 操作数(来自 xmm 寄存器)的存储永远不会转发到后续加载。
    因此,对于转发和非转发场景,尝试在寄存器内进行数据操作,并在 xmm 寄存器中进行累加。例如,在 mp3 解码器中,有一个窗口循环来累积/计算寄存器中的和,然后跨寄存器进行求和。
    这会在 16 字节存储到 pSum 数组和随后的 4 字节从 pSum 加载之间产生一个阻塞的存储转发停顿。为避免这种情况,可以在 xmm 寄存器中计算水平和,使用 HADDPS 指令或一系列加法和移位指令。(但请注意,HADDPS 序列在 Intel® Atom™ 处理器上速度更快,但在许多 Intel Core 处理器变体上速度较慢)。利用 SSE 的 min 和 max 指令对超过 16 位范围的样本进行裁剪。
  12. 在使用 XMM 寄存器之前,将其完全清零(MOVLPS, MOVHPS, PINSRW),因为某些指令只加载寄存器的一部分,这可能导致其他部分的代码未被清除而产生问题。
  13. 阅读关于优化 SIMD 指令(Intel SSE vs. ARM* NEON*)的 文章。考虑使用此处提供的 NEONvsSSE_5.h 库(代替 arm_neon.h)。文章还提到,处理常量时存在性能损失(不要将初始化放在循环中,如果可能,替换为逻辑/比较操作),并避免串行实现(使用向量化)。
  14. 替换耗费大量时钟周期的除法和 sqrt 运算,改为使用表查找运算、倒数近似(RCPPS 指令)或牛顿-拉夫逊序列。
  15. 注意浮点调用。使用 Float 而不是 Double(因为 Double 通常使用 SW 库例程)。Float 更快,并且可以在 Intel® Atom™ 处理器上节省内存带宽。此外,APP_ABI 设置是使用 SW(armeabi)还是 HW(X86,armeabi-v7a x86)浮点。您不总是希望使用 x86 算法执行完整的 HW FPU 计算。(例如,在整数代码中除以 2 的幂是一个快速的右移操作,但为了 Android 优化,您应该乘以其倒数(y=x*.5 而不是 y=x/2)。

  16. 减少小函数开销。在参数传递、新堆栈帧设置/旧堆栈帧恢复/保留调用者堆栈帧、将地址压入堆栈、返回值调用和返回函数等区域使用内联函数。另请参阅《Intel® 64 和 IA-32 架构优化参考手册》。
© . All rights reserved.