链表的替代品—内存池

链表是你们很是熟悉的数据结构,使用频率也很是高,可是链表有几个缺点。首先,咱们每建立一个节点,都要进行一下系统调用分配一块空间,这会浪费一点时间;其次,因为建立节点的时间不固定,会致使节点分配的空间不连续,容易造成离散的内存碎片;最后,因为内存不连续,因此链表的局部访问性较差,容易出现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缺失问题。操作系统

相关文章
相关标签/搜索