链表的替代品—内存池

链表是大家非常熟悉的数据结构,使用频率也非常高,但是链表有几个缺点。首先,我们每创建一个节点,都要进行一下系统调用分配一块空间,这会浪费一点时间;其次,由于创建节点的时间不固定,会导致节点分配的空间不连续,容易形成离散的内存碎片;最后,由于内存不连续,所以链表的局部访问性较差,容易出现cache缺失。
针对链表的上述问题,在实际工作中,我们很少直接用链表,而是采用链表的替代品—内存池。上过操作系统课程的同学对内存池应该不陌生,而且应该也知道设计一个好的内存池非常麻烦。但替换链表的内存池做了很大的简化:它是单线程的而且是只支持固定块大小的内存池。在具体实现过程中,我们先分配N块固定大小的连续内存,N块需要根据需求设定,之后每需要一个节点就从内存池中get一块空闲的块,用完之后再回收到内存池中。
内存池可以解决链表三个问题中的前两个,不能解决后一个,但是如果内存池较小可以缓解cache缺失的问题,整体而言还是可以很好地代替链表。下面看一下具体实现。

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表示可分配块的末尾。

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; iindex_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对应位置的块返回即可。

/**
 * @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即可。

/**
 * @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缺失问题。

你可能感兴趣的:(数据结构与算法)