GNU2.7 STL二级空间配置器的源码剖析

1.源码

下面是分配器简化后的源码

enum {__ALIGN = 8};                         //小区块上调边界
enum {__MAX_BYTES = 128};                   //小区块的上限
enum {__NFREELISTS = __MAX_BYTES/__ALIGN};  //free-list个数

template<bool threads, int inst>
class __default_alloc_template{
private:
    // 向上取8的倍数,比如bytes=13,则return 16
    static size_t ROUND_UP(size bytes){ 
        return ((bytes + __ALIGN - 1) & ~(__ALIGN - 1));
    }
    union obj {
        union obj* free_list_link;
    };
    
	// 不同大小的free list,总共有128/8=16个
    static obj* volatile free_list[__NFREELISTS];
    // 通过申请的字节数计算出对应的free list编号
    static size_t FREELIST_INDEX(size_t bytes){
        return ((bytes + __ALIGN - 1) / __ALIGN - 1);
    }

    // 重新填充free list,并返回新的区块的起始地址  
    static void* refill(size_t n);
    // 分配nobjs个大小为size的区块,并返回一个指向这些区块中的任意一个的指针,nobjs可能会减小
    static char* chunk_alloc(size_t size, int &nobjs);

    // chunk allocation state
    static char* start_free;    // 内存池头指针
    static char* end_free;      // 内存池尾指针
    static size_t heap_size;    // 分配累计量

public:
    static void* allocate(size_t n) // n must be > 0
    {
        obj* volatile *my_free_list; // 指向free list的指针
        obj* result;
		// 大于128字节,直接调用malloc
        if(n > (size_t)__MAX_BYTES){
            return malloc(n);
        }
		// 根据申请内存大小,选择对应的free list
        my_free_list = free_list + FREELIST_INDEX(n);
        result = *my_free_list;
        // 如果list为空,调用refill填充free list,并返回一个区块的起始地址
        if(result == 0){
            void* r = refill(ROUND_UP(n));
            return r;
        } 
        // 从list中取出一个可用区块,并将list指向下一个区块
        *my_free_list = result->free_list_link;
        return result;
    }
	// 内存回收函数
    static void deallocate(void* p, size_t n)
    {
        obj* q = (obj*)p;
        obj* volatile *my_free_list; // 指向free list的指针
        // 如果n大于128字节,直接调用free
        if(n > (size_t)__MAX_BYTES){
            free(p);
            return;
        }
        // 根据n计算free list数组下标,找到对应的free list
        my_free_list = free_list + FREELIST_INDEX(n);
        // 将释放的内存块加入到对应的free list中,成为链表头
        q->free_list_link = *my_free_list;
        *my_free_list = q;
    }
};

template<bool threads, int inst>
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;  // 改变需求数(nobjs是pass-by-reference)
        total_bytes = size * nobjs;
        result = start_free;
        start_free += total_bytes; // 更新内存池的起始位置
        return result;
    }else{                          // 内存池剩余空间不足以满足一块需求
        size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4); // 需要从系统中申请的字节数

        // 首先尝试将内存池做充分利用
        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);
        if( 0 == start_free){ // 如果申请失败
            int i;
            obj* volatile *my_free_list;
            obj* p;

            // 尝试从自由链表中寻找合适的空闲内存块,分配给客户
            for(i = size; i <= __MAX_BYTES; i += __ALIGN){
                my_free_list = free_list + FREELIST_INDEX(i);
                p = *my_free_list;
                if(0 != p){ // 该自由链表有可用区块,释放其中一块给内存池
                    *my_free_list = p->free_list_link;
                    start_free = (char*)p;
                    end_free = start_free + i;
                    return chunk_alloc(size, nobjs); //递归再试一次
                }
            }

            // 如果自由链表中没有可用内存块,则将内存池标记为空
            end_free = 0;

            // 再次尝试从系统中申请新内存,并注入到内存池
            start_free = (char*)malloc(bytes_to_get);
        }
        // 在成功从系统中申请到内存后,更新堆大小
		heap_size += bytes_to_get;
		// 更新内存池的末尾位置
		end_free = start_free + bytes_to_get;
		// 递归再次尝试分配内存块,因为现在内存池中有足够的空间
		return chunk_alloc(size, nobjs);
    }
}

// 重新填充指定大小的内存池,n已调整至8的倍数
template <bool threads, int inst>
void *__default_alloc_template<threads, inst>::refill(size_t n)
{
    int nobjs = 20;                      // 预设20个区块
    char *chunk = chunk_alloc(n, nobjs); // 从内存池中获取nobjs个大小为n的区块,nobjs是引用传递
    obj *volatile my_free_list;          // 定义指向指针的指针,用于指向指定大小的free list
    obj result;                          // 用于返回可用区块的指针
    obj *current_obj;                    // 当前处理的对象
    obj *next_obj;                       // 下一个对象
    int i;
    if (1 == nobjs)
        return chunk; // 如果实际得到的区块数只有1,将其直接交给客户使用
    // 以下开始将所得区块挂上free-list
    my_free_list = free_list + FREELIST_INDEX(n); // 找到对应大小的free list的地址
    result = (obj *)chunk;                        // 将第一个区块作为返回结果
    my_free_list = next_obj = (obj)(chunk + n);   // 将第二个区块设置为该free list的第一个区块
    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; // 返回可用区块的指针
}

// 分配器的静态成员变量初始化
template<bool threads, int inst>
char* __default_alloc_template<threads, inst>::start_free = nullptr;

template<bool threads, int inst>
char* __default_alloc_template<threads, inst>::end_free = nullptr;

template<bool threads, int inst>
size_t __default_alloc_template<threads, inst>::heap_size = 0;

template<bool threads, int inst>
__default_alloc_template<threads, inst>::obj* volatile
__default_alloc_template<threads, inst>::free_list[__NFREELISTS]
    = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};

//定义分配器别名alloc
typedef __default_alloc_template<false, 0> alloc;

2.源码剖析

GNU2.7 STL中的allocator机制是通过维护16个free list,每个list的元素大小不同,从8字节到128字节依次增长。当需要分配内存时,如果要分配的内存大小小于等于128字节,则从对应的free list中分配空间;如果待分配元素的大小超过128字节,则直接调用malloc分配空间。

  1. 假设我们要申请24字节内存,根据计算,需要从list#2中分配。但此时list#2为空,所以调用refill给list#2分配内存。refill内部调用chunk_alloc申请960字节(960
    = 2 * 20 * 24 + ROUND_UP(heap_size))的内存,其中第一个区块返回给客户,19个区块挂到list#2上,余下480字节作为pool备用。同时,记录当前已分配的内存大小为960字节,heap_size=960
  2. 接下来,如果再申请24字节的内存,则从list#2中取一块返回给客户,list#2中还剩余18个区块(432字节)。
  3. 如果需要申请64字节的内存,则需要从list#7中分配。但是此时list#7为空,所以调用refill。由于pool的空间大小为480字节,可以分配7个64字节的区块给list#7,第一个区块返回给客户,pool剩余大小为32字节。
  4. 如果需要申请48字节的内存,则需要从list#5中分配。但是此时list#5为空,所以调用refill。检查发现pool的空间32字节不足以满足申请需求,所以先把pool中的剩余空间利用起来,32字节作为一个区块挂到list#3上。然后重新申请2880字节的内存,其中第一个区块返回给客户,19个区块挂到list#5上,余下1920字节作为pool备用。
  5. 假设此时pool已经空了,系统可用内存只剩24字节,如果需要申请40字节的内存,则需要从list#4中分配。但是此时list#4为空,所以调用refill。检查发现pool的空间0字节不足以满足申请需求,所以调用malloc申请2 * 20 * 40+ROUND_UP(heap_size)字节的内存。由于这个值大于系统可用内存,所以malloc失败。此时从list#5开始检查,如果有空间,就把第一个区块借给pool,然后递归调用chunk_alloc,从pool中分配空间返回给客户。

3.设计优点

使用GNU2.7 STL二级空间配置器来管理内存分配和释放,可以减少内存碎片的产生,避免内存被大量浪费.

  • 在内存池中,每个链表上的内存块被分成两部分:一部分被分配出去使用,另一部分作为备用。备用内存可以给当前的链表使用,也可以给其他链表使用。这样可以避免频繁的分配和释放内存,从而减少内存碎片的产生。
  • 当备用内存池划分完chunk块以后,如果还有剩余的很小的内存块,再次分配的时候,会把这些小块内存挂到合适的chunk链表上,这样可以提高内存利用率。
  • 如果分配指定字节数的内存失败,会有一个异常处理过程,会向后查找大于指定字节数的chunk链表,如果那个链表上有空闲的chunk块,就会直接借用一个出来使用。这样就可以更好地利用内存池中的备用内存,减少内存碎片的产生,从而提高系统的性能。

4.设计缺陷

这个内存池设计存在固有缺陷,因为它从单向链表头上取空间,已经返回给客户的空间不被记录下来,所以在设计上无法真正释放内存,只能重新将不再使用的内存插入链表头。这种设计在多任务系统中可能会导致很大的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值