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);
我的经验:如果你做的是相机类应用,比如实时滤镜、人脸检测,强烈建议用AHardwareBuffer。相机输出的数据本身就是AHardwareBuffer格式,直接传给NNAPI,零拷贝。我曾经把一个实时人脸检测的延迟从15ms降到了8ms,就是靠这个。

不过要注意,不是所有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);
核心要点:内存池优化的本质,是用空间换时间。多花几百KB内存,换来每帧几毫秒的延迟降低,在实时应用中非常值得。

7.4 知识体系总览

下面这张图,是我对NNAPI内存管理的整体理解:

NNAPI内存管理知识体系 共享内存 createFromFd AHardwareBuffer 零拷贝硬件共享 内存池优化 空间换时间 文件描述符 mmap映射 跨进程共享 用户态↔内核态 GPU直接访问 无需CPU拷贝 相机数据 原生格式支持 固定大小池 预分配复用 分桶策略 动态适配 选择建议 • 简单场景(CPU推理):共享内存 + 固定内存池 • 复杂场景(GPU/NPU推理):AHardwareBuffer + 分桶内存池 • 实时流处理:异步预分配 + 双缓冲机制

7.5 避坑指南

最后,分享几个我踩过的坑:

  1. 内存泄漏:ANeuralNetworksMemory_free必须在所有Execution都完成后调用。我曾经在异步执行时提前释放了Memory,结果驱动崩溃,查了两天才找到原因。
  2. 对齐问题:共享内存的偏移量必须是页对齐的(通常4KB)。如果你传了一个非对齐的偏移,某些驱动会静默失败,返回错误结果。
  3. 生命周期管理:AHardwareBuffer的引用计数要小心。NNAPI内部会增加一次引用,如果你在创建Memory后立刻release了AHardwareBuffer,后续使用时会访问野指针。
  4. 驱动兼容性:不同厂商的NNAPI驱动对内存管理的实现有差异。高通、联发科、三星,各有各的脾气。建议在真机上充分测试。
我的建议:如果你刚开始接触NNAPI内存管理,先从共享内存+固定内存池开始。等跑通了,再逐步引入AHardwareBuffer和更复杂的池化策略。一口吃不成胖子,内存优化也是。

好了,这一章的内容就到这里。内存管理是NNAPI性能优化的基石,值得你花时间好好打磨。


公众号:蓝海资料掘金营,微信deep3321