STL源码剖析(三)第二级空间配置器

STL源码剖析(三)第二级空间配置器


上一篇文章中,我们讨论了空间配置器的作用,还有看了第一级空间配置器的源码,接下来我们来剖析这个第二级空间配置器的源码

一、为什么需要第二级空间配置器?

首先思考一下,为什么需要新的空间配置器?

因为在第一级空间配置器中,是直接采用malloc和free进行内存的申请和释放,这样做的优点是简单,但是它有很大的缺点。

一方面,mallc申请内存的时候,系统会附加一小块cookie来记录这块内存的大小,如果每次分配的内存都很小,那么这块cookie占总内存的比例就很大,使得内存的使用率不高

另一方,如果频繁地分配小块内存,那么势必会造成内存的碎片化,这是我们不愿意看到的

STL为了解决这个问题,实现了另一个空间配置器default_alloc_template,它对内存做了精细的管理

理论上这个空间配置器非常的好,但令人疑惑的是,STL并没有沿用下来,反而使用的是上一篇文章讲的第一级空间配置器,但是这并不妨碍我们学习这个优秀的空间配置器

二、源码剖析

2.1 初步分析

这个空间配置器是default_alloc_template,其定义如下

class __default_alloc_template {
private:
    /* 管理内存块的节点 */
    union obj {
        union obj * free_list_link;
        char client_data[1];
    };

    /* 管理内存的指针数组 */
    static obj * __VOLATILE free_list[__NFREELISTS];
    
    /* 维护内存池 */
    static char *start_free;
    static char *end_free;
    static size_t heap_size;

public:
    static void * allocate(size_t n)
    {
        ...
    }

    static void deallocate(void *p, size_t n)
    {
        ...
    }
};

default_alloc_template维护着一个内存池,内存池每次分配内存都会分配一大块内存,并维护free_listfree_list是一个指针数组,free_list有16项,每一项都维护一个对应大小的内存块链表,大小分别为8、16、24、32、40、48、56、64、72、80、88、96、104、112、120、128,如下图所示

在这里插入图片描述

free_list中的每个内存块节点使用obj管理,如下所示

union obj {
    union obj * free_list_link; //指向下一个内存块
    char client_data[1]; //表示数据
};

在这里插入图片描述

可以看到这里使用的是联合体而不是结构体,如果使用结构体,那么意味着每一个内存块都需要额外的一个指针来维护链表,而这个指针只有在free_list中有用,当这个内存块被分配出去后,这个指针也就没有作用了。这里使用联合体的原因也在于此,其目的是为了节省内存,当节点应用free_list_link则表示维护链表的指针,当节点引用client_data,则表示这一整块内存

当调用这个空间配置器的allocate分配内存的时候,如果要求的内存大于128,那么就会直接调用malloc进行分配,否则就会从free_list中获取(要求的内存大小会8字节对齐)

当调用deallocate释放内存的时候,如果大于128,那么就会直接调用free将其释放,否则会将这个内存块放入free_list当中,下面将一一分析

2.2 分配内存(allocate)

分配函数的定义如下

class __default_alloc_template {
    ...
  static void * allocate(size_t n)
  {
    obj * __VOLATILE * my_free_list;
    obj * __RESTRICT result;

    /* 如果大于128,则使用malloc */
    if (n > (size_t) __MAX_BYTES) {
        return(malloc_alloc::allocate(n));
    }
        
    /* 否则从free_list中获取内存,首先找到对应内存块大小的链表 */
    my_free_list = free_list + FREELIST_INDEX(n); //找到指定内存大小的free_list

    result = *my_free_list;
    if (result == 0) { //如果该链表上没有内存,那么就重新填充
        void *r = refill(ROUND_UP(n));
        return r;
    }
    
    /* 删除被分配的内存块 */
    *my_free_list = result -> free_list_link;
    return (result);
  };
};

上面的注释已经很清楚的,这里再理一下思路,首先,如果申请的内存大于__MAX_BYTES(128),那么就直接调用malloc进行分配。否则,从free_list找到管理指定内存块大小的链表,如果该链表上没有内存块,那么就重新填充,之后将分配得到的内存块从链表中删除,在返回此内存块

至于其中的refill怎么填充的,稍后会专门分析

2.3 释放内存 (deallocate)

class __default_alloc_template {
  static void deallocate(void *p, size_t n)
  {
    obj *q = (obj *)p;
    obj * __VOLATILE * my_free_list;

    /* 如果大于128,那么会直接使用free */
    if (n > (size_t) __MAX_BYTES) {
        malloc_alloc::deallocate(p, n);
        return;
    }
      
	/* 从free_list中找到指定块大小的链表 */
    my_free_list = free_list + FREELIST_INDEX(n);

    /* 将该内存块插入到该链表的最前端 */
    q -> free_list_link = *my_free_list;
    *my_free_list = q;
  }
};

上述程序中,首先如果内存块大于__MAX_BYTES(128),那么就会调用malloc_alloc::deallocate进行释放,其会直接调用free。否则,首先从free_list中找到指定内存块大小的链表,然后将要释放的内存块插入到该链表的最前端

2.4 free_list 填充(refill)

在分配内存时,我们没有分析refill函数

refill函数的作用是,从缓存块中获取内存,填充free_list对应的链表

void* __default_alloc_template<threads, inst>::refill(size_t n)
{
    int nobjs = 20;
    char * chunk = chunk_alloc(n, nobjs); //要求从缓存块中索要20个节点大小的内存
    
    /* 找到free_list中指定的链表 */
    my_free_list = free_list + FREELIST_INDEX(n);
    
    result = (obj *)chunk; //返回结果
    
    /* 将剩余的内存连接到free_list指定的链表上 */
    *my_free_list = next_obj = (obj *)(chunk + n);
    for (i = 1; ; i++) {
        current_obj = next_obj;
        next_obj = (obj *)((char *)next_obj + n);
        if (nobjs - 1 == i) {
            current_obj -> free_list_link = 0;
            break;
        } else {
            current_obj -> free_list_link = next_obj;
        }
    }
    
    return result;
}

首先会从缓存块中分配20个节点大小的内存,但是并不一定有20个节点大小,分配完之后,会将一个节点大小的内存返回,剩余的内存添加到free_list指定的链表中

2.5 缓存块(chunk_alloc)

下面再来看看缓存块是如何分配内存给的

其中start_free是缓存块起始处,end_free是缓存块结尾处

char*
__default_alloc_template<threads, inst>::chunk_alloc(size_t size, int& nobjs)
{
	char * result;
	size_t total_bytes = size * nobjs; //要求分配大小
	size_t bytes_left = end_free - start_free; //缓存块剩余大小
    
    if (bytes_left >= total_bytes) { //如果缓存块剩余空间足够,那么就直接从缓存块获取内存
        result = start_free;
        start_free += total_bytes;
        return(result);
    } else if (bytes_left >= size) { //如果缓存块空间不足,但大于一个节点大小,就返回剩余最多的节点大小
    	nobjs = bytes_left/size;
        total_bytes = size * nobjs;
        result = start_free;
        start_free += total_bytes;
        return(result);
    } else { //如果缓存块大小不足一个节点的大小
        /* 将剩余空间添加到指定的free_list中 */
        if (bytes_left > 0) {
            obj * __VOLATILE * my_free_list =
                free_list + FREELIST_INDEX(bytes_left);
            ((obj *)start_free) -> free_list_link = *my_free_list;
            *my_free_list = (obj *)start_free;
        }
        
        /* 重新为缓存块分配内存 */
        start_free = (char *)malloc(bytes_to_get);
        heap_size += bytes_to_get;
        end_free = start_free + bytes_to_get;
        
        return(chunk_alloc(size, nobjs)); //递归调用
    }
}

case1:如果缓存块剩余大小大于指定节点数的大小,那么就直接从缓存块返回

case2:如果缓存块剩余大小不满足指定节点数的大小,但是大于1个节点的大小,那么就返回最大节点数的内存

case3:缓存块剩余大小小于1个节点的大小,那么就将剩余内存添加到指定的free_list链表中,然后重新分配大块内存,递归调用chunk_alloc

发布了107 篇原创文章 · 获赞 197 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/weixin_42462202/article/details/101116536
今日推荐