STL二级空间配置器

在前面博客已经实现了一级空间配置器,博客链接如下:

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中一级空间配置器和二级空间配置器的选择上,一般默认选择的为二级空间配置器。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值