鏈表的替代品—內存池

鏈表是你們很是熟悉的數據結構,使用頻率也很是高,可是鏈表有幾個缺點。首先,咱們每建立一個節點,都要進行一下系統調用分配一塊空間,這會浪費一點時間;其次,因爲建立節點的時間不固定,會致使節點分配的空間不連續,容易造成離散的內存碎片;最後,因爲內存不連續,因此鏈表的局部訪問性較差,容易出現cache缺失。
針對鏈表的上述問題,在實際工做中,咱們不多直接用鏈表,而是採用鏈表的替代品—內存池。上過操做系統課程的同窗對內存池應該不陌生,並且應該也知道設計一個好的內存池很是麻煩。但替換鏈表的內存池作了很大的簡化:它是單線程的並且是隻支持固定塊大小的內存池。在具體實現過程當中,咱們先分配N塊固定大小的連續內存,N塊須要根據需求設定,以後每須要一個節點就從內存池中get一塊空閒的塊,用完以後再回收到內存池中。
內存池能夠解決鏈表三個問題中的前兩個,不能解決後一個,可是若是內存池較小能夠緩解cache缺失的問題,總體而言仍是能夠很好地代替鏈表。下面看一下具體實現。java

1. 結構體

內存池想象中比較簡單,就是首先分配一塊大內存,每次取一小塊內存,用完再放回去便可,可是實現起來須要較多的輔助變量。咱們不能僅僅經過一個指針來完成對內存池的訪問,由於獲取和釋放的順序是隨機的。咱們須要標記每一塊的使用情況。內存池的結構體以下:數組

typedef struct _mem_pool_
{
    char*  buffer_arr;

    char** index_arr;
    char** index_cur;
    char** index_end;
}mem_pool_t;

buffer_arr就是原始的整塊大內存,除了這個變量以外還有三個二維指針:index_arr是一個指針數組,分別指向每一塊的首地址,index_cur是一個遍歷指針,用來指向當前可分配的塊,index_end表示可分配塊的末尾。markdown

2. 初始化

初始化須要指定塊大小和塊個數,而後分配大塊內存。但初始化的關鍵操做是初始化幾個二維指針。看一下代碼:數據結構

/** * @brief 建立內存池 * @param capacity 容量 * @param block_size 對象單元大小 * @return 內存池對象指針,若是建立失敗返回NULL */
mem_pool_t* mem_pool_init(int capacity, int unit_size)
{
    int i = 0;
    char *work = NULL;
    mem_pool_t* mem_pool = NULL;

    if(capacity <=0 || unit_size <=0)
    {
        printf("Illegal params, capacity[%d]"
                    " unit_size[%d]", capacity, unit_size);
        return NULL;
    }

    mem_pool = (mem_pool_t*)malloc(sizeof(mem_pool_t));
    if(NULL == mem_pool)
    {
        printf("init memery pool failed");
        return NULL;
    }

    mem_pool->buffer_arr = (char*)malloc(capacity*unit_size);
    if(NULL == mem_pool->buffer_arr)
    {
        printf("Failed to alloc mem_pool buffer");
        return NULL;
    }

    mem_pool->index_arr = (char**)malloc(sizeof(char*)*capacity);
    if(NULL == mem_pool->index_arr)
    {
        printf("Failed to alloc memory pool "
                    "index array");
        return NULL;
    }
    work = mem_pool->buffer_arr;
    for(i=0; i<capacity; i++, work+=unit_size)
    {
        mem_pool->index_arr[i] = work;
    }
    mem_pool->index_end = mem_pool->index_arr + capacity;
    mem_pool->index_cur = mem_pool->index_arr;

    return mem_pool;
}

index_arr是一個和內存池容量同樣的二維指針,它被初始化爲內存池每一塊的首地址。index_end指向index_arr末尾的下一個位置,用來表示內存池的末尾。index_cur只是簡單地等於index_arr的首地址。後面咱們就能夠經過加減index_cur來分配或回收一塊內存。函數

3. get函數

get函數的目的是從內存池中取一塊可用內存,它首先判斷是否還有可用塊,若是有就返回當前可用塊。返回可用塊的方法很簡單,只須要將index_cur對應位置的塊返回便可。ui

/** * @brief 從內存池中分配一個單元 * @param mem_pool 內存池指針 * @return 新分配的對象指針,若是分配失敗返回NULL */
void* mem_pool_alloc(mem_pool_t* mem_pool)
{
    void* ret;

    if(mem_pool->index_cur >= mem_pool->index_end)
    {
        printf("memory pool overflow");
        return NULL;
    }
    ret = *(mem_pool->index_cur++);

    return ret;
}

4. free函數

free函數的目的是回收一塊用完的內存。和get相反,咱們只須要把回收內存的地址賦給index_cur-1便可。spa

/** * @brief 內存池回收一個對象單元 * @param mem_pool 內存池 * @param obj 待回收的對象單元 * @return errno * 0 : OK * -1 : ERROR */
int mem_pool_free(mem_pool_t* mem_pool, void* obj)
{
    if(NULL == mem_pool)
    {
        printf("try to free block in NULL pool");
        return MEM_POOL_ERR;
    }
    if(NULL == obj)
    {
        printf("try to free NULL object");
        return MEM_POOL_ERR;
    }

    *(--mem_pool->index_cur) = (char*)obj;

    return MEM_POOL_OK;
}

這裏須要注意的是,index_arr一開始是順序指向每一塊內存的,可是在不停地get和free過程當中index_arr開始亂序指向每一塊內存。
能夠看出,上面的get和free操做都很是簡單,只是簡單的加減操做,因此速度很是快,並且內存池是一整塊內存,不存在內存碎片的問題。同時,若是內存池較小,也能夠很大程度上緩解cache缺失問題。操作系統

相關文章
相關標籤/搜索