在前面博客已经实现了一级空间配置器,博客链接如下:
http://blog.csdn.net/l_xrui/article/details/64500898
1.首先明白为什么需要二级空间配置器?
我们知道动态开辟内存时,要在堆上申请,但若是我们需要频繁的在堆上开辟小块内存,释放,开辟释放,则就会再堆上造成很多外部碎片,浪费了内存空间;并且由于每次都要进行调用malloc、free函数等操作,使空间就会增加一些附加信息,降低了空间利用率;并且随着外部碎片增多,内存分配器在找不到合适内存情况下需要合并空闲块,浪费了时间,大大降低了效率。于是就设置了二级空间配置器,当开辟内存<=128bytes时,即视为开辟小块内存,则调用二级空间配置器。
2.二级空间配置器的实现
主要过程:即为了防止不断的在堆上开辟释放小块内存,二级空间配置器则是先在堆上申请一大块的侠义内存池,并用一个自由链表(free_list)来管理这块内存池,当我们需要小块内存时,则直接在free_list上寻找相应大小的内存块,返回其地址交由我们使用,若释放时则将内存块直接插入到自由链表的相应位置。为了方便管理,二级空间配置器会主动将用户需求的小额区块提升为8的倍数(如你需要20bytes,就会自动调整给你24bytes),并维护16个free_list,各自管理区块大小为8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128。
如图:
为什么自由链表管理内存最小从8开始,当自由链表某一位置存储的obj*指针指向内存池某一地址时,内存池这块内存则要在存储一个obj类型,由上图可知,obj类型中又有一个obj*指针,而在系统32位下指针大小为4,64位则为8,兼容性的考虑,则将内存管理从8字节开始。
从上图也可知,obj类型为union,所以obj可以视为一个指针,指向相同形式的一个obj;它又可以当做一个地址,指向内存的实际区块,实现了“一物二用”的效果。
代码如下:
//二级空间配置器
template <bool threads, int inst>
class _DefaultAllocTemplate
{
private:
//查找自由链表中适当大小的内存块的下标
//33+7=40/8=5-1=4(减1下标从0开始)
static size_t FREELIST_INDEX(size_t bytes)
{
return (((bytes) + __ALIGN-1)/__ALIGN - 1);
}
//将开辟的内存大小提升为8的倍数
//若bytes==10;10+8-1=17&~(8-1)=0001 0001&1111 1000=0001 0000=16
//即将最低三位变为0
static size_t ROUND_UP(size_t bytes)
{
return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1));
}
public:
static void * Allocate(size_t n)
{
obj** my_free_list;
obj* result;
//大于128则调用一级空间配置器
if(n>(size_t)__MAX_BYTES)
{
return MallocAlloc::Allocate(n);
}
//查找自由链表中适当大小的内存块
size_t index=FREELIST_INDEX(n);
my_free_list=free_list;
result=my_free_list[index];
//若自由链表为空则从内存池填充
if(result==0)
{
result=(obj*)Refill(ROUND_UP(n)); //将n自动提升为8的倍数
return result;
}
//不为空则调整自由链表,头删
my_free_list[index]=result->free_list_link;
return result;
}
static void Deallocate(void *p, size_t n)
{
obj* q=(obj*) p;
obj** my_free_list;
//若大于128调用一级空间配置器
if(n>(size_t)__MAX_BYTES)
{
MallocAlloc::Deallocate(p,n);
return;
}
//重新挂上自由链表,头插
my_free_list=free_list;
size_t index=FREELIST_INDEX(n);
q->free_list_link=my_free_list[index];
my_free_list[index]=q;
}
private:
//填充自由链表
static void* Refill(size_t size)
{
//默认从内存池开辟20块size大小内存
int nobjs=20;
char* chunk=ChunkAlloc(size,nobjs);
//若内存池只能开辟一块
if(nobjs==1)
{
return chunk;
}
//若内存池开辟一块以上,则返回第一块,在自由链表挂上剩下的内存块
char* result=chunk;//第一块直接返回
obj** my_free_list=free_list;
size_t index=FREELIST_INDEX(size);
my_free_list[index]=(obj*)(chunk+size);//直接挂第二块
//挂剩下的nobjs-2块
obj* cur=my_free_list[index];
obj* next;
for(size_t i=2;i<nobjs;++i)
{
next=(obj*)(chunk+size*i);
cur->free_list_link=next;
cur=next;
}
cur->free_list_link=0;
return result;
}
//从内存池切分内存给自由链表
static char* ChunkAlloc(size_t size, int &nobjs)
{
char* result;
size_t total_bytes=size*nobjs; //开辟内存的总大小
size_t bytes_left=end_free-start_free; //内存池剩余的内存总大小
//1.若内存池剩余空间满足需求20块size大小
if(bytes_left>=total_bytes)
{
result=start_free;
start_free+=total_bytes; //调整内存池开始位置
return result; //返回自由链表所需的内存开始地址
}
//2.内存池剩余内存不够20块size大小,但可以分配一块以上
else if(bytes_left>=size)
{
nobjs=bytes_left/size; //调整默认所需内存块的个数
total_bytes=nobjs*size;
result=start_free;
start_free+=total_bytes;
return result;
}
//3.内存池剩余空间一块size大小都无法满足,即内存池为空或几乎已经用完
else
{
//若内存池还有零头,将其挂上自由链表的适当位置
if(bytes_left>0)
{
size_t index=FREELIST_INDEX(bytes_left);//查找位置
//头插
obj** my_free_list=free_list;
((obj*)start_free)->free_list_link=my_free_list[index];
my_free_list[index]=(obj*)start_free;
}
//从堆上申请大块内存池
size_t bytes_to_get=2*total_bytes+ROUND_UP(heap_size>>4);//即申请的bytes_to_get大小一定为8的倍数
//由于size为8的倍数,即8的倍数*整数+8的倍数=8的倍数
//由此也得出内存池最后剩下的零头必定为8的倍数
start_free=(char*)malloc(bytes_to_get);
//申请失败
if(start_free==0)
{
//搜寻自由链表中>=size的内存块
size_t index=FREELIST_INDEX(size);
obj** my_free_list=free_list;
for(size_t i=index;i<__NFREELISTS;++i)
{
if(my_free_list[i])//找到
{
free_list[i]=my_free_list[i]->free_list_link;//头删
//分给内存池
start_free=(char*)my_free_list[i];
end_free=start_free+((i+1)*__ALIGN);
//递归调用自己修正nobjs;
return ChunkAlloc(size,nobjs);
}
}
//若自由链表也没有找到合适内存,山穷水尽,调用一级空间配置器
end_free=0;
start_free=(char*)MallocAlloc::Allocate(bytes_to_get);
}
//申请成功,修正内存池,递归调用自身
heap_size+=bytes_to_get;
end_free=start_free+bytes_to_get;
return ChunkAlloc(size,nobjs);
}
}
private:
static char *start_free; //内存池头指针
static char *end_free; //内存池尾指针
static size_t heap_size; //从堆上申请的内存总大小
private:
enum {__ALIGN = 8};
enum {__MAX_BYTES = 128};
enum {__NFREELISTS = __MAX_BYTES/__ALIGN};
union obj
{
union obj* free_list_link;
char client_data[1];
};
private:
static obj* free_list[__NFREELISTS]; //自由链表
};
//静态成员变量初始化
template <bool threads, int inst>
char *_DefaultAllocTemplate<threads, inst>::start_free = 0;
template <bool threads, int inst>
char *_DefaultAllocTemplate<threads, inst>::end_free = 0;
template <bool threads, int inst>
size_t _DefaultAllocTemplate<threads, inst>::heap_size = 0;
template <bool threads, int inst>
typename _DefaultAllocTemplate<threads, inst>::obj * _DefaultAllocTemplate<threads, inst>::
free_list[__NFREELISTS] = {0};
template<class T, class Alloc>
class SimpleAlloc
{
public:
static T *Allocate(size_t n)//开辟多个对象
{
return 0 == n? 0 : (T*) Alloc::Allocate(n * sizeof (T));
}
static T *Allocate(void)//开辟一个对象
{
return (T*) Alloc::Allocate(sizeof (T));
}
static void Deallocate(T *p, size_t n)
{
if (0 != n)
Alloc::Deallocate(p, n * sizeof (T));
}
static void Deallocate(T *p)
{
Alloc::Deallocate(p, sizeof (T));
}
};
typedef _DefaultAllocTemplate<false,0> DefaultAlloc;
typedef __MallocAllocTemplate<0> MallocAlloc;
void Test()
{
vector<int*> v;
SimpleAlloc<int,DefaultAlloc> sa;
for(int i=0;i<20;++i)
{
v.push_back(sa.Allocate());
}
v.push_back(sa.Allocate());
vector<int*>::iterator it=v.begin();
while(it!=v.end())
{
printf("%x\n",*it);
++it;
}
for(int i=0;i<=20;++i)
{
sa.Deallocate(v[i]);
}
}
主要实现具体过程:
1.请求n大小内存块,调用allocate()函数,在函数中先判断n是否>=128,若大于,则调用一级空间配置器,否则,就根据n求出所需内存块在自由链表管理的位置下标,查找自由链表,若不为空,则直接摘下这块内存,即返回链表中所存储的这块内存的地址;若为空,则将n提升为8的倍数,调用refill()函数,填充自由链表此位置。
2.在refill()函数中,首先定义nobjs=20,即调用chunk_alloc()函数,默认向内存池申请20块大小为n(注意此处n已被提升为8的倍数)的内存,若申请以后,只申请了一块即nobjs=1(在chunk_alloc()函数中会更新块数nobjs)时,则直接将这块内存地址返回去使用,不必在挂在自由链表上,若申请一块以上,则返回第一块内存供使用,将剩下的nobjs-1块挂在自由链表上,若下次需要相同大小内存块则直接在自由链表上摘下使用。
3.在调用chunk_alloc()函数时,即向内存池申请空间给自由链表时,首先会算出所需内存的总大小total_bytes=n*nobjs与内存池剩余空间bytes_left,相比较,若
3.1.bytes_left>=total_bytes,即内存池足够20块大小n使用,则直接申请total_bytes大小内存,更新内存池大小返回refill()函数;
3.2.bytes_left>=n,即内存池只足够供应一块或一块以上的内存,但不足20块,这时更新nobjs,重新算出total_bytes,然后申请total_bytes大小内存,更新内存池,返回;
3.3.bytes_left<n,即内存池一个内存块都无法供应时:
(1)此时先查看内存池是否有零头内存(此处零头内存一定为8的倍数,原因在与在堆上申请内存池的大小时也为8的倍数,即在使用时全部都是以8的倍数在使用,所以最后剩下的必定也为8的倍数),若有则先将它挂上自由链表的适当位置;
(2)在堆上申请malloc()内存池的大小(申请的大小请查看代码):
若堆上申请失败时,此时则查找从自由链表当前位置开始以后是否有挂着的空闲内存块,若有,则将其摘下重新赋予内存池,在递归调用自身,将会执行3.2情况;
若自由链表也没有内存块后,山穷水尽时将会调用一级空间配置器,若在一级空间配置器里处理后重新申请内存成功则成功,若失败则抛出异常;
若申请成功,则更新内存池,在递归调用自身,将会执行3.1情况。
4.最后在释放时调用deallocate()函数,若释放的n>128,则调用一级空间配置器,否则就直接将内存块挂上自由链表的合适位置。
STL二级空间配置器虽然解决了外部碎片与提高了效率,但它同时增加了一些缺点:
1.因为自由链表的管理问题,它会把我们需求的内存块自动提升为8的倍数,这时若你需要1个字节,它会给你8个字节,即浪费了7个字节,所以它又引入了内部碎片的问题,若相似情况出现很多次,就会造成很多内部碎片;
2.二级空间配置器是在堆上申请大块的狭义内存池,然后用自由链表管理,供现在使用,在程序执行过程中,它将申请的内存一块一块都挂在自由链表上,即不会还给操作系统,并且它的实现中所有成员全是静态的,所以它申请的所有内存只有在进程结束才会释放内存,还给操作系统,由此带来的问题去1.即我不断的开辟小块内存,最后整个堆上的空间都被挂在自由链表上,若我想开辟大块内存就会失败,2.若自由链表上挂很多内存块没有被使用,当前进程又占着内存不释放,这时别的进程在堆上申请不到空间,也不可以使用当前进程的空闲内存,由此就会引发多种问题。
最后关于STL中一级空间配置器和二级空间配置器的选择上,一般默认选择的为二级空间配置器。