首页 > 代码库 > 一个非常简单的C++内存池方案

一个非常简单的C++内存池方案

在游戏中频繁使用new与delete将会导致性能的下降,还可能造成内存碎片。

使用以一个自定义的内存分配器将是很重要的。


创建一个通用又强大效率性能又高的内存分配器将是困难的,所以这个的分配器面向下面的情况使用:

1. 一开始就就申请,直到游戏退出才释放


这种情况我们无需做分配器内部的内存释放,仅需将分配器本身释放掉即可


2. 申请后立即释放


当然不算是立即释放,不然就没用了。

这种情况一般是需要一个临时缓冲区,或者使用一个临时对象。

我们的内存可以记录上次申请的情况,方便重复利用内存资源。



当然,我们还需要支持一个强大的功能——任意字节对齐

内存对齐对提高性能很有帮助,特别地,有些地方对字节对齐有硬性要求。

比如SIMD(单指令流多数据流)操作矩阵或者矢量时,就要求128位即16字节对齐。

我们就仅需将这些数据排到前面即可。


特别地,如果使用继承特别是继承抽象类需要特别注意数据在内存的排列方法。


比如继承一个抽象类,那么在数据的前4字节(32位程序)将会是虚表指针,需要16字节对齐的话,

需要在前面写一些有用没用数据,比如指针啥的。


好了,放代码,不足百行

// 只申内存池 
// EachPoolSize : 每片缓冲池的大小 略大于实际能分配内存数
template<size_t EachPoolSize=128 * 1024> class AllocOnlyMPool{
    // 节点
    struct Node{
        // 后节点
        Node*               next = nullptr;
        // 已分配数量
        size_t              allocated = 0;
        // 上次分配位置
        BYTE*               last_allocated = nullptr;
        // 缓冲区
        BYTE                buffer[0];
    };
public:
    // 构造函数
    AllocOnlyMPool(){
        assert(m_pFirstNode && "<AllocOnlyMPool::AllocOnlyMPool>:: null m_pFirstNode");
    }
    // 析构函数
    ~AllocOnlyMPool();
    // 申请内存
    void*               Alloc(size_t size, UINT32 align = sizeof(size_t));
    // 释放内存
    void                Free(void* address);
private:
    // 申请节点
    static __forceinline Node* new_Node(){
        auto* pointer = reinterpret_cast<Node*>(malloc(EachPoolSize));
        pointer->Node::Node();
        return pointer;
    }
    // 首节点
    Node*       m_pFirstNode = new_Node();
};



// **实现**

// 申请内存
template<size_t EachPoolSize>
void* AllocOnlyMPool<EachPoolSize>::Alloc(size_t size, UINT32 align){
    if (size > (EachPoolSize - sizeof(Node))){
#ifdef _DEBUG
        assert(!"<AllocOnlyMPool<EachPoolSize>::Alloc>:: Alloc too big");
#endif
        return nullptr;
    }
    // 获取空闲位置
    auto* now_pos = m_pFirstNode->buffer + m_pFirstNode->allocated;
    // 获取对齐后的位置
    auto aligned = (reinterpret_cast<size_t>(now_pos)& (align - 1));
    if (aligned) aligned = align - aligned;
    now_pos += aligned;
    // 增加计数
    m_pFirstNode->allocated += size + aligned;
    // 检查是否溢出
    if (m_pFirstNode->allocated > (EachPoolSize - sizeof(Node))){
        Node* node = new_Node();
        if (!node) return nullptr;
        node->next = m_pFirstNode;
        m_pFirstNode = node;
        // 递归(仅一次)
        return Alloc(size, align);
    }
    // 记录上次释放位置
    m_pFirstNode->last_allocated = now_pos;
    return now_pos;
}


// 释放内存 
template<size_t EachPoolSize>
void AllocOnlyMPool<EachPoolSize>::Free(void* address){
    // 上次申请的就这样了
    if (address && m_pFirstNode->last_allocated == address){
        m_pFirstNode->allocated =
            (m_pFirstNode->last_allocated - m_pFirstNode->buffer);
        m_pFirstNode->last_allocated = nullptr;
    }
}


// AllocOnlyMPool 析构函数
template<size_t EachPoolSize>
AllocOnlyMPool<EachPoolSize>::~AllocOnlyMPool(){
    Node* pNode = m_pFirstNode;
    Node* pNextNode = nullptr;
    // 顺序释放
    while (pNode)
    {
        pNextNode = pNode->next;
        free(pNode);
        pNode = pNextNode;
    }
}


顺序说明一下:

1. 使用模板,参数是每个单元的大小,因为储存了数据,实际每个单元能够分配的内存量略小,

看实际情况赋予参数吧,128k一般够了。大不了上兆,反正内存白菜价了。

模板还可能增加目标程序大小,可改为变量。但是一般用一个模板示例就行了,就无所谓了


2.Node使用空数组(中括号里面啥也没有),标准C++支不支持我不知道,反正VC++警告了”非标准扩展“,

不过空数组是在C99里面允许的


3.对指针行进位操作进行对齐操作,虽说是任意字节对齐。

但是一般得低于4k,因为一般操作系统一页的大小为4k,高于4k将毫无意义。



使用方法: 实例化对象。  需要全局使用的话就用静态变量。





多线程支持。


游戏一般将会是多线程的,所以应该对多线程进行支持。


但是上锁与解锁需要时间,违背的这个内存池的设计初衷。

所以一般游戏推荐的是“无锁操作”,所以在这就不对其进行上锁。




那怎么保证安全呢?


我们仅需保证对其进行互斥操作——为每个需要这个内存池的线程分配一个内存池即可


这就是对象内存池的好处

一个非常简单的C++内存池方案