在 Android 模拟器 Shell 下运行 ncnn 推理的性能排查记录

在 Android 模拟器 Shell 下运行 ncnn 推理的性能排查记录

本文是对 《ChineseOCR Lite Android 纯C++版 - 初学者学习项目》 中一个未解问题的深入探讨。在那篇文章中,我将 chineseocr_lite 的 Android 版本去掉了 Java/JNI 层,编译为独立的 main 可执行文件,通过 adb shell 直接在 Android 设备上运行。文章末尾记录了一个现象:在模拟器上通过 App 调用很快,但直接通过 Shell 调用就很慢,当时仅做了初步猜测,并未深入排查。本文将完整还原这个问题的排查过程。

背景

在上一篇文章中,我完成了 chineseocr_lite 的纯 C++ 移植,去掉了所有 Java 层,使用标准 main 入口和命令行参数接收图片路径,支持在 Android 真机和模拟器上直接运行。

编译目标架构为 arm64-v8a,推送到模拟器执行后,模型能跑通,但速度极慢,处理一张 1080P 图片需要几秒。而同样的模型在手机 App 上却很快。为了找出原因,我进行了一系列排查。

排查过程

1. 为什么 App 调用快,Shell 调用慢?

最初我怀疑是 GPU 加速的问题。在代码中设置 net.opt.use_vulkan_compute = true 后,App 端能运行且 CPU 占用率不高,而 Shell 端 CPU 直接拉满。

我通过 logcat 抓取日志分析:

vbnet 复制代码
vulkan version is 4206869
DbNet use vulkan compute
Load vulkan library directly. so 0x31254b642b2d987d
Failed to get gpu service
searching for layers in '/data/app/.../lib/x86_64'
no vulkan device

日志中的 vulkan version is 4206869 是标准的 Vulkan API 版本编码(对应 1.3.277),仅凭版本号无法判断具体是哪种 Vulkan 实现(硬件驱动、软件模拟层或其他)。但 Failed to get gpu serviceno vulkan device 表明 ncnn 最终没有找到可用的 Vulkan 物理设备,实际回退到了 CPU 计算。

因此,App 下并没有真正使用 GPU 硬件加速。 那为什么 App 比 Shell 快?

真正的原因是 Android 的 ABI 自动选择机制

当通过 JNI/App 调用时,Android 的包管理系统会根据设备架构自动从 APK 中选择最合适的 so 库。我的 APK 中同时包含了 x86_64 和 arm64-v8a 等多种架构的库。在 x86_64 的电脑模拟器上,系统会自动选择 x86_64 版本的 so,实现原生执行,无需指令集转译。

而 Shell 中直接运行的 main 可执行文件,是我单独编译的 arm64-v8a 版本。在 x86_64 的电脑模拟器上运行 ARM 程序时,必须经过指令集转译层实时翻译,这个开销非常大,导致速度极慢。

运行方式 实际执行架构 是否需要转译 速度
App (JNI) x86_64(系统自动选择) 否,原生执行
Shell (./main) arm64-v8a(强制运行) 是,实时转译 极慢

2. 为什么手机上的模拟器比电脑模拟器快?

同一个 arm64-v8a 的 main 程序,在手机上安装的模拟器(如 VMOS、光速虚拟机等)的 Shell 里运行很快,但在电脑模拟器(如 MuMu)的 Shell 里很慢。

原因分析:

环境 宿主机架构 虚拟机内架构 程序架构 执行方式
手机模拟器(VMOS等) ARM64 ARM64 arm64-v8a 原生执行
电脑模拟器(MuMu等) x86_64 x86_64 arm64-v8a 指令集转译

手机本身是 ARM 架构,手机上的虚拟机里跑的也是 ARM 系统。因此 arm64-v8a 的程序在手机模拟器里是原生执行,没有转译开销。

而电脑是 x86_64 架构,在 Shell 中运行 ARM 程序时,必须经过转译层逐条翻译指令。特别是 ncnn 大量使用了 ARM NEON 向量指令,转译这类指令的效率极低,即使开启多线程,每个线程都在做"翻译+执行",整体速度依然很慢。

3. 编译 x86_64 后的动态库链接问题

既然电脑模拟器是 x86_64 架构,我尝试将 main 编译为 x86_64 版本,绕过指令集转译。

编译成功后再次运行,遇到了新的报错:

vbnet 复制代码
CANNOT LINK EXECUTABLE "./main": library "libicu.so" not found: needed by /system/lib64/libharfbuzz_ng.so in namespace (default)

程序依赖了 OpenCV,而 OpenCV 中的某些模块(如涉及文字渲染的 highgui/harfbuzz)底层依赖了系统的 libicu.so。由于 Android 的命名空间隔离机制,默认情况下 Shell 进程的 Linker 无法访问系统级库。

我尝试从系统中拷贝 libicu.so 并设置 LD_LIBRARY_PATH:

bash 复制代码
cp /system/lib64/arm64/libicu.so ./
export LD_LIBRARY_PATH=/sdcard/Download:$LD_LIBRARY_PATH
./main

结果报出架构不匹配的错误:

java 复制代码
"/storage/emulated/0/Download/libicu.so" is for EM_AARCH64 (183) instead of EM_X86_64 (62)

原因分析:

我拷贝的 /system/lib64/arm64/libicu.so 是模拟器专门为转译层准备的 ARM64 库,而当前运行的 main 是 x86_64 架构,Linker 拒绝加载不同架构的动态库。

4. 定位正确的 x86_64 系统库

我重新在系统中搜索 libicu.so

bash 复制代码
find /system -name libicu.so
# /system/etc/mumu-configs/translators/38816/lib64/arm64/libicu.so
# /system/lib64/arm64/libicu.so
# /system/apex/com.android.i18n/lib64/libicu.so

前两个路径都带有 arm64 标识,是给转译层用的。而 /system/apex/ 目录是 Android 较新版本用来存放核心系统模块的地方,这里的库才是对应系统架构(即 x86_64)的原生库。

由于 Shell 进程默认不会搜索 APEX 路径,我需要手动将这个路径加入环境变量:

bash 复制代码
export LD_LIBRARY_PATH=/system/apex/com.android.i18n/lib64/:$LD_LIBRARY_PATH
./main s.raw

这次程序顺利运行,输出结果:

less 复制代码
width: 1080, height: 1920
size:8294400
use time:446.956
ocr result: 温馨提示

执行时间从原来的几十秒骤降到了 446 毫秒,达到了正常水平。

总结

在 Android 模拟器的 Shell 下运行 C++ 推理程序,有几个关键点需要注意:

  1. App 与 Shell 的性能差异本质: 不是 GPU 加速,而是 ABI 架构匹配问题。APK 包含多种架构的 so 库,Android 系统会自动为设备选择最优架构(如 x86_64 模拟器选 x86_64 so),实现原生执行;而 Shell 中直接运行的单一架构可执行文件,在跨架构时必须经过指令集转译,性能损失巨大。

  2. 架构必须匹配: 在 x86_64 电脑模拟器上,务必编译 x86_64 架构的程序。跨架构的指令集转译会带来巨大的性能损耗,导致多线程也难以奏效。而手机上的 ARM 虚拟机不存在这个问题,因为 arm64-v8a 程序可以原生执行。

  3. 动态库依赖与隔离: Android 的命名空间隔离会阻止 Shell 访问部分系统库。如果遇到 not found 且确认库存在于系统中,需排查是否是架构不匹配(误用了 arm64 目录下的库),或是需要通过 LD_LIBRARY_PATH 显式指定类似 /system/apex/... 这种被隐藏的库路径。

此外,对于纯推理的命令行程序,建议在 CMake 阶段就剥离 OpenCV 中不必要的 UI 模块(如 highgui),可以从源头减少这类系统级动态库的依赖问题。

相关推荐
落羽的落羽1 小时前
【项目】JsonRpc框架——开发实现1(细节功能、字段定义、抽象层、具象层)
linux·服务器·网络·c++·人工智能·算法·机器学习
shixuzhimeng2 小时前
FTP服务器项目
linux·网络·ftp
Chris-zz2 小时前
Linux:线程概念与控制
linux·运维
剑神一笑2 小时前
Linux chown 命令详解:从 inode 到实战
linux·运维·服务器
MIXLLRED2 小时前
随笔——在 Ubuntu 22.04 中查看 Markdown (.md) 文件
linux·运维·ubuntu·markdown
STDD2 小时前
Linux cgroup v2 资源控制实战:限制进程 CPU/内存/IO,systemd slice 管理
linux·运维·服务器
kukubuzai3 小时前
Docker Note
linux·运维·docker
Ltd Pikashu3 小时前
insmod 加载内核模块 —— sys_init_module 源码剖析
linux·kernel·insmod
hj2862514 小时前
Linux网络基础一
linux·运维