在 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 service 和 no 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),可以从源头减少这类系统级动态库的依赖问题。

相关推荐
琥珀.7 小时前
mysql中json数组转列表查询
linux·运维·服务器
dawnsky.liu7 小时前
在 RHEL 中使用 goose 实现远程主机管理
linux·运维·ai
Linux运维技术栈9 小时前
CVE-2026-91403 高危漏洞修复:Rocky Linux 9 多版本 JDK 环境清理实操
java·linux·运维
slongzhang_11 小时前
linux 递归删除源码文件|递归删除目录下 *.js,但排除 *.min.js
linux·开发语言·javascript
叼烟扛炮11 小时前
Linux 学习笔记(三十三):线程互斥——互斥锁(Mutex)与死锁
linux·运维·服务器·笔记·学习·1024程序员节
无足鸟ICT11 小时前
【RHCA+】who命令(查看已登陆用户)
linux·运维·服务器
Escalating_xu11 小时前
【Linux 网络编程】应用层协议 HTTP(下):Header、连接管理与 C++ HTTP 服务器
linux·服务器·网络
GeW14 小时前
新质生产力路线图落地,Linux运维与数据库架构师的机会清单
linux
Brilliantwxx14 小时前
【Linux】 文件访问与路径解析:目录、文件名、dentry、路径缓冲与 dcache 全解析
linux·运维·服务器·开发语言·spring
father_122714 小时前
Greenplum 集群故障恢复与主备同步机制实战
linux·网络·greenplum