7、内存管理(Memory):共享内存、AHardwareBuffer支持、内存池优化策略
内存管理这个话题,在NNAPI开发中可以说是「兵家必争之地」。为什么?因为AI推理的本质,就是数据在CPU、GPU、NPU之间来回搬运。搬运效率高,模型就跑得快;搬运效率低,哪怕你的模型再牛,也白搭。
我个人习惯把NNAPI的内存管理分成三个层次:共享内存是基础,AHardwareBuffer是进阶,内存池是终极优化。咱们一层一层剥开来看。
7.1 共享内存:ANeuralNetworksMemory_createFromFd
先说说最基础的共享内存。NNAPI里,模型输入输出的数据,不能直接传指针,必须通过ANeuralNetworksMemory对象来传递。这个对象背后,其实就是一块共享内存。
创建共享内存有两种方式:
ANeuralNetworksMemory_createFromFd:从文件描述符创建ANeuralNetworksMemory_createFromAHardwareBuffer:从AHardwareBuffer创建(后面细说)
先看第一种,也是最常用的方式:
// 分配一块匿名共享内存
int fd = ashmem_create_region("nnapi_memory", buffer_size);
if (fd < 0) {
// 处理错误
return;
}
// 映射到进程空间
void* buffer = mmap(NULL, buffer_size, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
// 创建NNAPI Memory对象
ANeuralNetworksMemory* memory = nullptr;
ANeuralNetworksMemory_createFromFd(buffer_size, PROT_READ | PROT_WRITE,
fd, 0, &memory);
// 填充数据
float* input_data = static_cast<float*>(buffer);
input_data[0] = 1.0f;
// ... 填充更多数据
// 使用完毕后释放
ANeuralNetworksMemory_free(memory);
munmap(buffer, buffer_size);
close(fd);
ANeuralNetworksMemory_createFromFd调用成功后,NNAPI内部会持有这个fd的引用。你不能在创建后立刻close(fd),要等到Memory对象释放后才能关。否则在某些驱动实现上会出段错误。
为什么一定要用共享内存?说白了,NNAPI的执行是在驱动层,和你的App进程不在同一个地址空间。直接传指针,驱动根本访问不到。共享内存就是一座桥,让数据在用户态和内核态之间自由流动。
7.2 AHardwareBuffer支持
共享内存虽然好用,但有个硬伤——它只适合CPU访问。如果你的数据要直接在GPU或NPU上处理,共享内存就不够看了。这时候,AHardwareBuffer就登场了。
AHardwareBuffer是Android 8.0引入的,专门用于跨硬件组件共享内存。它支持GPU、视频编解码器、显示系统等硬件直接访问,不需要经过CPU拷贝。
在NNAPI中,你可以这样用:
// 创建AHardwareBuffer
AHardwareBuffer_Desc desc = {};
desc.width = 224; // 图像宽度
desc.height = 224; // 图像高度
desc.layers = 1;
desc.format = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM;
desc.usage = AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN |
AHARDWAREBUFFER_USAGE_GPU_SAMPLER;
AHardwareBuffer* hw_buffer = nullptr;
AHardwareBuffer_allocate(&desc, &hw_buffer);
// 创建NNAPI Memory
ANeuralNetworksMemory* nn_memory = nullptr;
ANeuralNetworksMemory_createFromAHardwareBuffer(hw_buffer, &nn_memory);
// 设置输入
ANeuralNetworksExecution_setInputFromMemory(execution, 0, nullptr,
nn_memory, 0, buffer_size);
// 执行推理
ANeuralNetworksExecution_startCompute(execution, &wait_event);
// 释放
ANeuralNetworksMemory_free(nn_memory);
AHardwareBuffer_release(hw_buffer);
不过要注意,不是所有NNAPI驱动都支持AHardwareBuffer。你可以通过ANeuralNetworksDevice_getCapabilities查询设备是否支持ANEURALNETWORKS_FEATURE_LEVEL_3及以上。如果不支持,就老老实实用共享内存。
7.3 内存池优化策略
好了,基础讲完了,咱们聊聊真正的「优化」——内存池。
你想想看,每次推理都分配、释放内存,频繁的mmap/munmap操作,开销其实不小。尤其是做连续推理时,比如视频流处理,每帧都来一次内存分配,性能直接拉胯。
内存池的思路很简单:提前分配好一块大内存,重复使用。
7.3.1 固定大小内存池
如果你的模型输入输出大小固定,这是最简单的方案:
class NNAPIMemoryPool {
private:
struct PoolEntry {
ANeuralNetworksMemory* memory;
void* buffer;
bool in_use;
};
std::vector<PoolEntry> pool_;
std::mutex mutex_;
public:
NNAPIMemoryPool(size_t pool_size, size_t buffer_size) {
for (size_t i = 0; i < pool_size; ++i) {
// 创建共享内存
int fd = ashmem_create_region("pool", buffer_size);
void* buf = mmap(NULL, buffer_size, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
ANeuralNetworksMemory* mem = nullptr;
ANeuralNetworksMemory_createFromFd(buffer_size,
PROT_READ | PROT_WRITE,
fd, 0, &mem);
close(fd);
pool_.push_back({mem, buf, false});
}
}
PoolEntry* acquire() {
std::lock_guard<std::mutex> lock(mutex_);
for (auto& entry : pool_) {
if (!entry.in_use) {
entry.in_use = true;
return &entry;
}
}
return nullptr; // 池满了,需要等待或扩容
}
void release(PoolEntry* entry) {
std::lock_guard<std::mutex> lock(mutex_);
entry->in_use = false;
}
~NNAPIMemoryPool() {
for (auto& entry : pool_) {
ANeuralNetworksMemory_free(entry.memory);
munmap(entry.buffer, buffer_size_);
}
}
};
7.3.2 动态适配内存池
如果模型大小不固定,比如你同时跑多个不同模型,固定大小池就不够灵活了。我建议用「分桶」策略:
- 把内存按大小分成几个桶:1MB以下、1-4MB、4-16MB、16MB以上
- 每个桶里维护一个空闲链表
- 申请时找最合适的桶,释放时归还
这样做的好处是,既避免了频繁分配,又不会浪费太多内存。
7.3.3 异步预分配
还有一个技巧,我称之为「异步预分配」。在视频流处理场景中,你可以提前分配好N帧的内存:
// 假设帧率30fps,我们预分配3帧的内存
const int kPrefetchFrames = 3;
std::queue<ANeuralNetworksMemory*> ready_queue;
// 后台线程预分配
std::thread prefetch_thread([&]() {
while (running) {
if (ready_queue.size() < kPrefetchFrames) {
auto* mem = allocate_memory();
ready_queue.push(mem);
}
std::this_thread::sleep_for(5ms);
}
});
// 主线程直接取用
auto* mem = ready_queue.front();
ready_queue.pop();
// 用完后归还
ready_queue.push(mem);
7.4 知识体系总览
下面这张图,是我对NNAPI内存管理的整体理解:
7.5 避坑指南
最后,分享几个我踩过的坑:
- 内存泄漏:ANeuralNetworksMemory_free必须在所有Execution都完成后调用。我曾经在异步执行时提前释放了Memory,结果驱动崩溃,查了两天才找到原因。
- 对齐问题:共享内存的偏移量必须是页对齐的(通常4KB)。如果你传了一个非对齐的偏移,某些驱动会静默失败,返回错误结果。
- 生命周期管理:AHardwareBuffer的引用计数要小心。NNAPI内部会增加一次引用,如果你在创建Memory后立刻release了AHardwareBuffer,后续使用时会访问野指针。
- 驱动兼容性:不同厂商的NNAPI驱动对内存管理的实现有差异。高通、联发科、三星,各有各的脾气。建议在真机上充分测试。
好了,这一章的内容就到这里。内存管理是NNAPI性能优化的基石,值得你花时间好好打磨。
公众号:蓝海资料掘金营,微信deep3321