两种内存池技术(C++实现)

一、概述

C++相较于其他高级语言来讲,能够方便的进行内存管理和操作,是其优势也是其劣势,运用得当将使得你编写的程序性能大大提升,使用不当也可能给你带来无尽的麻烦。内存池就是其中的重要技术手段之一,下面重点看看常见的两种内存池技术。

二、实现

1、基于某种类型的内存池

此种内存池使用广泛,实现相对简单,基本能够满足大部分时候的需求,使用模板实现,保证了一种内存池针对一种类型来分配内存,内存池中存储的对象占用空间大小一致,省去了许多在给对象分配内存和释放内存时的操作。主要原理如下:
第一种内存池实现原理主要由两个链表和两个指针对整个内存池进行控制,current_block链表将所有分配的memory_block链接起来,便于整个内存池在析构阶段的内存释放操作;free_slot将所有已分配后来又释放掉的内存区域链接起来,便于该区域的二次利用,当需要分配内存时,首先从free_slot链表中查询是否有空闲区域,若没有,则直接从current_slot处取得内存进行分配,若current_slot已经超过last_slot,则意味着所有向系统申请的memory_block都已经用完,需要申请新的memory_block。
代码实现如下:
memory_pool.h

#ifndef UNTITLED_MEMORY_POOL_H
#define UNTITLED_MEMORY_POOL_H

const size_t BlockSize=4096;

template <typename T>
class MemoryPool {
public:
    typedef T* pointer;

    MemoryPool();

    ~MemoryPool();

    void *allocate();

    void deallocate(pointer p);

private:
    union Slot_ {
        T elemetn;
        Slot_* next;
    };

    typedef Slot_ slot_type_;

    typedef Slot_* slot_pointer_;

    slot_pointer_ current_block_;

    slot_pointer_ free_slot_;

    slot_pointer_ current_slot_;

    slot_pointer_ last_slot_;
};

MemoryPool::MemoryPool() :current_block_(nullptr),free_slot_(nullptr),current_slot_(nullptr),last_slot_(nullptr) {};

MemoryPool::~MemoryPool() {
    while(current_block_!= nullptr){
        auto pre = current_block_->next;
        operator delete(reinterpret_cast<void*>(current_block_));
        current_block_=pre;
    }
}

void* MemoryPool::allocate() {
    if(free_slot_!= nullptr){
        auto ret = free_slot_;
        free_slot_=free_slot_->next;
        return reinterpret_cast<void*>(ret);
    }

    if(current_slot_>=last_slot_){
        auto new_block = reinterpret_cast<char*>(operator new(BlockSize));
        reinterpret_cast<slot_pointer_ >(new_block)->next = current_block_;
        current_block_ = reinterpret_cast<slot_pointer_ >(new_block);
        size_t body_padding = 0;
        //TODO memory alignment
        current_slot_ = new_block+ sizeof(slot_type_)+body_padding;
        last_slot_ = reinterpret_cast<slot_pointer_ >(new_block + BlockSize - sizeof(slot_type_)+1);
    }

    return reinterpret_cast<void*>(current_slot_++);
}

void MemoryPool::deallocate(pointer p) {
    if(nullptr != p) {
        reinterpret_cast<slot_pointer_ >(p)->next=free_slot_;
        free_slot_ = reinterpret_cast<slot_pointer_ >(p);
    }
}

#endif //UNTITLED_MEMORY_POOL_H

2、STL内存管理中应用到的内存池

STL中内存分配用到了两级配置器:
一级配置器:
第一级配置器用於单次分配大于128Byte的内存,主要以malloc(),free(),realloc()等C函数执行实际的内存配置、释放、重新配置等操作,不多作赘述。
二级配置器:
二级配置器的实现相对复杂一些,但主要运行流程还是比较简单的:
在这里插入图片描述
主要需要搞清楚的是链表头数组和内存链表的概念以及refill的工作原理,先看前者:
二级配置器中含有一个链表头数组,包含了16个成员,每个成员都是一个链表头,而每个链表头所指向的链表中的每个节点都指向了一块可用内存,16个链表头指向的链表的不同之处在于,它们中的节点指向的可用内存大小是不同的,例如:第一个链表头指向的链表中的节点指向的是8Byte大小的可用内存;第二个链表头指向的链表中的节点指向的是16Byte大小的可用内存,以此类推,如下图所示:
在这里插入图片描述上图中obj是一个union,其定义如下:

    union obj
    {
        union obj * free_list_link;
        char client_data[1];
    };

当某个内存块为空闲时,该内存块头部保存着一个obj的对象,该obj指向下一块空闲内存块,如此便构成了一个空闲内存块的链表,在需要分配内存时,先根据需要分配内存的大小在链表头数组中找到对应内存块大小的链表头(例如需要分配1-8Byte内存,则取出第一个链表头,若需要分配9-16Byte内存,则取出第二个,以此类推),之后便用该链表中取出空闲内存的指针返回即可,若链表已经没有可用节点,则调用refill。

static void * allocate(size_t n)
{
    obj * __VOLATILE * my_free_list;
    obj * __RESTRICT result;
 
    if (n > (size_t) __MAX_BYTES)    //大于128Byte调用一级配置器
    {
        return(malloc_alloc::allocate(n));
    }
    my_free_list = free_list + FREELIST_INDEX(n);   //根据需要分配内存的大小在链表头数组中找到对应内存块大小的链表头
 
    result = *my_free_list;
    if (result == 0)   //若链表已经没有可用节点
    {
        void *r = refill(ROUND_UP(n));//重新分配空间
        return r;
    }
    *my_free_list = result -> free_list_link;//并把空闲内存指针链表的指针指向下一个数据块
    return (result);
};

再看看refill:

template <bool threads, int inst>
void* refill(size_t n)
{
    int nobjs = 20;
    char * chunk = chunk_alloc(n, nobjs);//尝试从内存池里取出nobjs个大小为n的数据块,返回值nobjs为真实申请到的数据块个数,这里取出的所有内存块在空间上是连续的
    obj * __VOLATILE * my_free_list;
    obj * result;
    obj * current_obj, * next_obj;
    int i;
 
    if (1 == nobjs) return(chunk);//如果只获得一个数据块,那么这个数据块就直接分给调用者,空闲链表中不会增加新节点
    my_free_list = free_list + FREELIST_INDEX(n);//根据需要分配内存的大小在链表头数组中找到对应内存块大小的链表头
 
    result = (obj *)chunk;
    *my_free_list = next_obj = (obj *)(chunk + n);//由于第0个数据块直接给调用者了,空闲链表头指向下一个内存块即chunk + n(将指针指向地址后移n个byte)
    for (i = 1; ; i++)//将所有取出的内存块插入到空闲链表
    {
        current_obj = next_obj;
        next_obj = (obj *)((char *)next_obj + n);//由于之前取出的内存块里申请到的空间连续,因此同上,next_obj+n即为下一个内存块
 
        if (nobjs - 1 == i)
        {
            current_obj -> free_list_link = 0;
            break;
        }
        else
        {
            current_obj -> free_list_link = next_obj;
        }
    }
 
    return(result);
}

关于chunk_alloc分配内存就不多赘述了,STL源码解析中有很好的解析,其主要流程就是根据需要分配的内存从自己的内存池中取出相应的量返回,若内存池中存量不够,则继续向系统申请来进行补充。

三、总结

以上两种内存池技术各有优劣,第一种在实现上更为简单,流程清晰简介,但只适用于针对每种固定大小的类型;第二种为STL中使用,基本能够兼顾适配不同申请内存大小的需求和性能两者,但整体实现相对复杂,理解起来需要一定的时间。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章