如何在C++中实现一个自定义的内存池?

在高性能系统或游戏开发中,频繁的内存分配与释放会导致碎片化、缓存未命中以及 GC 触发。为了解决这些问题,可以自定义一个内存池(Memory Pool),在预先分配一大块内存后按需划分。下面给出一个简单、可扩展的实现示例,并解释关键点与使用方式。

1. 内存池类的设计思路

  • 预分配大块:在构造函数里使用 operator newmalloc 申请一段连续内存。
  • 管理空闲块:可以采用链表或自由列表(Free List)来记录未使用的块。每个块头部保存指向下一个空闲块的指针。
  • 对齐:C++ 对象需要特定对齐,内存池也需保证对齐。
  • 线程安全:如果多线程使用,可加入互斥锁或使用无锁技术。

下面实现一个最简版本:支持固定大小块的池,块数可在构造时指定。

2. 代码实现

#include <cstddef>
#include <cassert>
#include <mutex>
#include <new>        // std::bad_alloc
#include <cstring>    // std::memset

class FixedSizePool
{
public:
    explicit FixedSizePool(std::size_t blockSize, std::size_t blockCount)
        : m_blockSize((blockSize > sizeof(FreeBlock*)) ? blockSize : sizeof(FreeBlock*)),
          m_blockCount(blockCount),
          m_pool(nullptr),
          m_freeList(nullptr)
    {
        allocatePool();
    }

    ~FixedSizePool()
    {
        ::operator delete(m_pool, std::align_val_t(m_blockSize));
    }

    void* allocate()
    {
        std::lock_guard<std::mutex> lock(m_mutex);

        if (!m_freeList) {
            throw std::bad_alloc(); // pool exhausted
        }

        // pop one block
        FreeBlock* block = m_freeList;
        m_freeList = m_freeList->next;
        return static_cast<void*>(block);
    }

    void deallocate(void* ptr)
    {
        std::lock_guard<std::mutex> lock(m_mutex);
        if (!ptr) return; // ignore null

        // push back into free list
        FreeBlock* block = static_cast<FreeBlock*>(ptr);
        block->next = m_freeList;
        m_freeList = block;
    }

    // 禁止拷贝/移动
    FixedSizePool(const FixedSizePool&) = delete;
    FixedSizePool& operator=(const FixedSizePool&) = delete;

private:
    struct FreeBlock
    {
        FreeBlock* next;
    };

    void allocatePool()
    {
        // align allocation to block size
        m_pool = ::operator new(m_blockSize * m_blockCount, std::align_val_t(m_blockSize));

        // Initialize free list
        std::uint8_t* ptr = static_cast<std::uint8_t*>(m_pool);
        for (std::size_t i = 0; i < m_blockCount; ++i) {
            FreeBlock* block = reinterpret_cast<FreeBlock*>(ptr);
            block->next = m_freeList;
            m_freeList = block;
            ptr += m_blockSize;
        }
    }

    std::size_t m_blockSize;
    std::size_t m_blockCount;
    void* m_pool;
    FreeBlock* m_freeList;
    std::mutex m_mutex;
};

关键点说明

  1. 对齐
    ::operator new(size_t, std::align_val_t) 兼容 C++17 及以后版本,能保证内存块对齐。若使用低版本,可手动使用 posix_memalignaligned_alloc

  2. 空闲链表
    通过 FreeBlock 结构把空闲块串联,避免额外的头文件开销。allocate 时弹出链表首部,deallocate 时将块压回。

  3. 线程安全
    使用 std::mutex 包裹每次分配/释放操作,简单实现。若性能要求更高,可考虑使用 std::atomic 及无锁队列。

  4. 错误处理
    当池已满时抛 std::bad_alloc,可根据需求自行返回空指针或扩容。

3. 使用示例

int main()
{
    const std::size_t blockSize  = 64;   // 每块 64 字节
    const std::size_t blockCount = 1024; // 预分配 1024 块

    FixedSizePool pool(blockSize, blockCount);

    // 申请 10 块
    std::vector<void*> blocks;
    for (int i = 0; i < 10; ++i) {
        blocks.push_back(pool.allocate());
        std::memset(blocks.back(), 0, blockSize); // 可写
    }

    // 释放
    for (void* ptr : blocks) {
        pool.deallocate(ptr);
    }

    return 0;
}

4. 扩展思路

  • 可变大小块:将每块前加入一个 size_t 字段,记录大小;或者在块前存储元信息(如对象类型)供析构时调用。
  • 分层池:针对不同对象大小使用不同的固定池,减少碎片。
  • 内存池统计:加入使用计数、峰值等监控,便于调试。
  • 自定义分配器:实现 operator new / operator delete,使 STL 容器自动使用池。

5. 小结

自定义内存池能显著降低频繁分配导致的性能瓶颈,尤其在高频场景(如游戏对象、网络协议解析)中表现突出。上述实现以固定大小块为例,易于理解与扩展。结合线程安全与对齐需求,即可在实际项目中快速落地。

发表评论