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分配空间。
- 假设我们要申请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 - 接下来,如果再申请24字节的内存,则从list#2中取一块返回给客户,list#2中还剩余18个区块(432字节)。
- 如果需要申请64字节的内存,则需要从list#7中分配。但是此时list#7为空,所以调用refill。由于pool的空间大小为480字节,可以分配7个64字节的区块给list#7,第一个区块返回给客户,pool剩余大小为32字节。
- 如果需要申请48字节的内存,则需要从list#5中分配。但是此时list#5为空,所以调用refill。检查发现pool的空间32字节不足以满足申请需求,所以先把pool中的剩余空间利用起来,32字节作为一个区块挂到list#3上。然后重新申请2880字节的内存,其中第一个区块返回给客户,19个区块挂到list#5上,余下1920字节作为pool备用。
- 假设此时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.设计缺陷
这个内存池设计存在固有缺陷,因为它从单向链表头上取空间,已经返回给客户的空间不被记录下来,所以在设计上无法真正释放内存,只能重新将不再使用的内存插入链表头。这种设计在多任务系统中可能会导致很大的问题。