C++内存池(附源码)

本文深入探讨C++内存池的设计与实现,包括内存块管理、分配与回收机制、大块内存处理以及对象初始化。通过参考STL内存池,作者详细解释了如何通过链表管理和柔性数组技术减少内存损耗,同时提供了内存池的源码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

C++内存池(附源码)
前段时间阅读了Nginx的源码,其对内存高效的管理给我留下了深刻的印象,而内存管理的核心便是内存池。于是想自己实现一个C++版本的内存池,这方面当然还是STL的内存池最为经典,所以免不了参悟借鉴。内存池的概念早已经是老生常谈,然而把内存池实现的高效安全仍是个比较艰巨的问题。内存池的原理简单来讲就是一次性的向系统申请大量的内存,之后再有内存请求的时候,如果内存池的内存大小能够满足请求,就从内存池里分配,不必再进行系统调用,从而实现性能提升,而多次的内存申请系统调用,很容易生成内存碎片而造成内存浪费。池的概念大体如此,线程池,进程池无出其右。内存池的实现主要解决的问题有: 
1 内存池的块管理 
2 内存的分配和回收 
3 大块内存的分配和回收 
4 对象初始化

1 > 内存池的块管理 
这方面可以直接参考STL的分配器的实现,SGI STL在进行内存分配时,默认使用了一个内存池。这个内存池的内存块从8Byte开始,每递增8Byte都生成一系列链表管理的内存块,一直到128Byte结束。内存块定义为:

    union MemNode {
        MemNode*    _next;
        char        _data[1];
    };

union每个成员的起始地址都是开头的位置,所以每次仅能使用一个成员,在链表中由_next指向下个内存块的地址,在分配内存时由_data指向内存首地址,长度为1 的数组放在结构体最后一个成员位置,可以访问给结构体多分配的地址空间,这种技术叫做柔性数组。这样做的好处减少了对内存块管理时额外的内存损耗。想想我们学习数据结构时实现的链表,都是通过结构体的一个成员来指向下个节点的地址,多出了一个指针4Byte的内存消耗。参考STL,我们内存块的管理如下图所示: 


有同学要问了,那我要是申请比128更大的内存怎么办?SGI 这里就直接走正常的内存申请,还是会有系统调用产生。因为系统对于程序请求的内存,管理时也会生成额外的内存控制数据占用内存,这样申请的内存越小,额外占用的内存比例就越高。 我们每次申请指定量的内存,然后将内存格式化到块管理的数组链表中。

    char* res;
    size_t need_bytes = size * nums;
    size_t left_bytes = _pool_end - _pool_start;

    //内存池够用
    if (left_bytes >= need_bytes) {
        res = _pool_start;
        _pool_start += need_bytes;
        return res;

    } else if (left_bytes >= size) {
        nums = left_bytes / size;
        need_bytes = size * nums;
        res = _pool_start;
        _pool_start += need_bytes;
        return res;

    } 
    size_t bytes_to_get = size * nums;

    if (!is_large) {
        if (left_bytes > 0) {
            MemNode* my_free = _free_list[FreeListIndex(left_bytes)];
            ((MemNode*)_pool_start)->_next = my_free;
            _free_list[FreeListIndex(size)] = (MemNode*)_pool_start;
        }

    } else {
        free(_pool_start);
    }


    _pool_start = (char*)malloc(bytes_to_get);

    //内存分配失败
    if (0 == _pool_start) {
        throw std::exception("There memary is not enough!");
    }

    _malloc_vec.push_back(_pool_start);
    _pool_end = _pool_start + bytes_to_get;
    return ChunkAlloc(size, nums, is_large);

将返回的内存添加到块管理队列中

    my_free = &(_free_list[FreeListIndex(size)]);

    *my_free = next = (MemNode*)(chunk + size);
    for (int i = 1;; i++) {
        current = next;
        next = (MemNode*)((char*)next + size);
        if (nums - 1 == i) {
            current->_next = nullptr;
            break;

        } else {
                current->_next = next;
            }
        }

2 > 内存的分配和回收 
每次从系统申请内存时都通过一个辅助函数将内存增到为8的倍数,上层请求内存时寻找最小能容纳当前请求的头节点索引

   //获取size最小8的倍数
   size_t RoundUp(size_t size) {
        return ((size + __align - 1) & ~(__align - 1));
    }
    //获取容纳当前size的最小内存块索引
    size_t FreeListIndex(size_t size) {
        return (size + __align - 1) / __align - 1;
    }

当找到索引位置时,如果内存块不为空,则取出当前内存块,将之后的链表节点向前移动,如果内存不够的话,再次向系统请求新的内存。

    std::unique_lock<std::mutex> lock(_mutex);
    MemNode** my_free = &(_free_list[FreeListIndex(sz)]);
    MemNode* result = *my_free;
    if (result == nullptr) {
        void* bytes = ReFill(RoundUp(sz));
        memset(bytes, 0, sz);
        return bytes;
    }

    *my_free = result->_next;
    memset(result, 0, sz);
    return result;

内存回收时与此理相同,通过辅助函数找到索引位置,将内存块放入首部位置,之前的内存块后移。

    MemNode* node = (MemNode*)m;
    MemNode** my_free = &(_free_list[FreeListIndex(len)]);

    std::unique_lock<std::mutex> lock(_mutex);
    node->_next = *my_free;
    *my_free = node;
    m = nullptr;

3 > 大块内存的分配和回收 
通过以上的内存管理,我们足以解决小块内存的非配和回收,但是还可能存在另一种需求,类似Nginx内存池有大块内存的管理,我们在实际开发中也会用到诸如接收发送缓存之类的需求。这里添加一个新的列表 ,节点的内存大小和节点每次申请的数量都通过构造函数确定,从而支持特定大小内存的管理。

    MemNode*    _free_large;                        //bulk memory list
    int         _number_large_add_nodes;            //everytime add nodes num
    int         _large_size;                        //bulk memory size
1
2
3
原理与小块内存回收分配相似。

4 > 对象初始化 
与C语言实现内存池的不同之处在于,C语言可以只负责内存的分配而不用管内部数据的初始化,因为C语言没有对象的概念。但是在C++中,我们不仅仅要负责内存的分配,还要调用构造函数负责对象的初始化。大家知道C++中的 new操作符,一是负责内存申请,二是调用构造函数实现对象初始化。而C++中可以通过可变模板参数来实现任意数量任意参数的函数转发,再辅之std::forward完美转发,即可实现构造函数的调用功能。所以我实现的内存池对外提供内存申请的接口有三个:

    template<typename T, typename... Args >
    T* PoolNew(Args&&... args);
    template<typename T>
    void PoolDelete(T* &c);

    //for continuous memory
    void* PoolMalloc(size_t size);
    void PoolFree(void* m, size_t len);

    //for bulk memory
    void* PoolLargeMalloc();
    void PoolLargeFree(void* m);

这样每次请求和释放都需要调用接口,我觉得可以写一个基于内存池的智能指针,从而减少这些api的嗲用。还有这里为什么没有重载new操作符来呢?因为new和delete的重载函数只能是static函数(我想是因为new对象的时候,对象还没有创建),所以内存池的api通过重载new 和delete 实现,看起来很美好,但实际上是行不通的。我们要创建内存池的对象,每个内存池的对象管理的都是不同的内存。下面看下 PoolNew 调用构造函数的过程。

template<typename T, typename... Args>
T* CMemaryPool::PoolNew(Args&&... args) {
    int sz = sizeof(T);
    if (sz > __max_bytes) {
        void* bytes = malloc(sz);
        T* res = new(bytes) T(std::forward<Args>(args)...);
    return res;
    }

    std::unique_lock<std::mutex> lock(_mutex);
    MemNode** my_free = &(_free_list[FreeListIndex(sz)]);
    MemNode* result = *my_free;
    if (result == nullptr) {
        void* bytes = ReFill(RoundUp(sz));
        T* res = new(bytes) T(std::forward<Args>(args)...);
        return res;
    }
    *my_free = result->_next;
    T* res = new(result) T(std::forward<Args>(args)...);
    return res;
}

到这里基本上所有的功能都已经实现完毕。但是既然我们支持创建内存池的对象,那什么时候释放内存池占有的内存呢? 当然是析构函数中! 但是怎么释放呢? 我们是通过malloc 库函数申请的内存,释放的时候自然是去调用free释放。但是我们不能通过循环块的数组和链表去释放内存。因为我们申请的时候是一整块去申请的,释放的时候只要通过每次申请的头地址去释放即可。所以我在这里添加了一个辅助的std::vector来存储每次申请内存的地址,释放的时候只遍历这个std::vector即可。

    //声明
    std::vector<char*>  _malloc_vec;

    //存储
    _pool_start = (char*)malloc(bytes_to_get);
    if (0 == _pool_start) {
        throw std::exception("There memary is not enough!");
    }
    _malloc_vec.push_back(_pool_start);

    //释放
    for (auto iter = _malloc_vec.begin(); iter != _malloc_vec.end(); ++iter) {
        if (*iter) {
            free(*iter);
        }
    }

一些线程安全相关的内容没有在文章里提到,以上代码还没有经过充分的测试,如有错误的地方欢迎大家指出。我打算写一些列平时开发用到的工具,包括任务队列,线程模型,日志打印,基于内存池的智能指针之类。希望大家多多支持(●’◡’●)

以下是使用实例:

class test1 {
public:
    int aaaa;
    int bbbb;
    int cccc;
    int dddd;

    explicit test1(int a, int b, int c, int d):aaaa(a), bbbb(b), cccc(c), dddd(d){
        std::cout << "test1()" << std::endl;
    }
    ~test1() {
        std::cout << "~test1()" << std::endl;
    }
};

class test2 {
public:
    int aaaa;

    test2() {
        std::cout << "test2" << std::endl;
    }
    ~test2() {
        std::cout << "~test2()" << std::endl;
    }
};

int main() {
    CMemaryPool pool;
    test1* t1 = pool.PoolNew<test1>(1,2,3,4);
    t1->aaaa = 1000;
    t1->bbbb = 1000;
    t1->cccc = 1000;
    t1->dddd = 1000;
    pool.PoolDelete<test1>(t1);

    test2* t2 = pool.PoolNew<test2>();
    t2->aaaa = 1000;

    pool.PoolDelete<test1>(t1);
    pool.PoolDelete<test2>(t2);

    int len1 = sizeof(unsigned long);
    int len2 = sizeof(char*);

    int a;
    std::cin >> a;
}

最后贴出源码地址 
GitHub : https://github.com/caozhiyi/Base
--------------------- 
版权声明:本文为优快云博主「constCpp」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/u012778714/article/details/80299475

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值