兩種內存池技術(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中使用,基本能夠兼顧適配不同申請內存大小的需求和性能兩者,但整體實現相對複雜,理解起來需要一定的時間。

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