tiny_stl: :allocator之alloc

在STL中,对空间适配器allocator的操作分为两个部分,其中一个部分是alloc类提供的接口,分别负责内存配置(alloc: :allocate)和内存释放(alloc: :deallocate)。

设计需求:

  • 向堆申请空间
  • 需要考虑多线程状态(该问题比较复杂,这里不考虑)
  • 需要考虑内存不足时的措施
  • 需要考虑小型区块造成的内存碎片问题

向堆申请空间可以简单的使用malloc函数来完成,所以我们主要需要解决的问题就是最后两个,需要用到一定内存管理的知识。

内存碎片问题:
由于内存分配时要根据不同的标准进行对齐,所以通常会造成内存碎片的问题,为了解决这个问题,STL提供的配置器有两种配置方式,或者说是双层配置器。当需要分配的内存足够大时,内存分配造成的内存碎片影响较小,可以直接用malloc函数来分配,这是第一层适配器;当进行小型区块分配时,一旦分配次数较多,造成的内存碎片就会比较严重,我们需要通过某种内存管理手段来缓解这个问题。

解决方法:维护free_list(自由链表)来分配合适的内存块;内存块由内存池来分配
————————————————————————————————————————————

free_lists
free_lists包括16个free_list,换句话说就是一个包含16个链表的的链表数组,每个链表管理的内存大小均为8的倍数,这是因为内存分配默认8字节对齐,free_list的结点定义如下:

union obj {
	union obj *next;
	char client_data[1];
}

显然这是一个链表结构,不过为了节省空间,用了union来进行声明,不然使用了额外的指针又由占用了内存。这个结构的第二个字段看起来比较奇怪,它本质上代表了一个地址,这个地址就是该结点本身的地址,这样的定义并不代表这个结点实质的大小,举个例子:

obj *pobj = (obj*)malloc(64);

这样,由于pobj->client_data指向自身,它实际可访问64字节大小。
————————————————————————————————————————————

内存分配思路
如果用malloc直接分配,就会产生一些影响不小的外部碎片。比如,对于一块内存0~255,若一开始malloc分配了9字节,对齐后实质分配了16字节,即0 ~ 15,然后又分配8字节,即16 ~ 23;当我们free了0 ~ 15这块空间后,要malloc分配32字节,这时0 ~ 16这块空间就不够用了,只有在后方的地址进行分配,这样多次操作之后,中间的外部碎片的总和可能完全足以分配下一块空间,但由于不是连续的空间,导致这些碎片一直分配不出去,这就浪费了内存资源。

为了应对这种情况,使用了一个free list的结构。自由链表一共16个,其结点大小均为8的倍数,块大小依次递增,所以最大的块为128字节,因此,对于大于128字节的内存分配,我们直接使用malloc函数即可,对于不大于128字节的内存分配,就用自由链表中最合适的块去分配,比如需要5字节就实际分配8字节,需要124字节就实际分配128字节;而当我们释放一块内存时,就把这块内存作为结点插回free list中。这样多次分配和释放后只会存在一些小于7字节的内部碎片,而没有外部碎片。

那么,这些free list中的块或结点又是何时分配的呢?这里用到了内存池的概念,内存池提供了一个refill的接口,用来填充free list的结点,当某个free list的块已经分配干净后,再次需要分配时,就得从内存池中进行填充,默认填充的节点数是20个。
代码大概如下:

void *my_alloc::allocate(size_t n)
{
	if (n > 128)
	{
		return malloc(n);				//大于128直接用malloc分配
	}
	size_t index = FREELIST_INDEX(n);	//找到适合n字节的free list
	obj *list = free_list[index];
	if (list)
	{
		free_list[index] = list->next;
		return list;					//返回对应free list的第一个块
	}
	else
	{
		return refill(ROUND_UP(n));		//空间不够从内存池填充free list
	}
}

————————————————————————————————————————————

内存池

内存池起到两个作用

  1. 填充free list
  2. 考虑堆空间不足以分配时的措施

所谓内存池,本质上就是一开始使用malloc函数分配出来的一大块空间,它是free list构建的基础。内存池的空间由start_free和end_free两个指针来标识,它总是从开始处进行分配,每次填充free list后,start_free指针就向前移动分配的字节数。

填充free list会产生3种情况

  1. 内存池充足,可以完全提供默认的区块数。对于这种情况,直接返回这部分的内存空间即可。
  2. 内存池不充足,只能提供部分区块数。对于这种情况,把能提供的区块数空间返回即可。
  3. 内存池几乎耗尽,一个区块都无法提供。这种情况下,就必须重新向堆申请空间了。

代码大概如下:

char* my_alloc::chunk_alloc(size_t size, size_t& nobjs)
{
	char *ret = 0;
	size_t total_bytes = size * nobjs;
	size_t bytes_remain = end_free - start_free;	//内存池剩余空间

	if (bytes_remain > total_bytes)					 //内存池充足
	{
		ret = start_free;
		start_free += total_bytes;
		return ret;
	}
	else if (bytes_remain >= size) 					//内存池部分满足
	{
		nobjs = bytes_remain / size;
		total_bytes = size * nobjs;
		ret = start_free;
		start_free += total_bytes;
		return ret;
	}
	else                          					 //内存池提供不了1个区块
	{
		size_t bytes2get = 2 * total_bytes + ROUND_UP(heap_size >> 4);
		start_free = (char*)malloc(bytes2get);
		heap_size += bytes2get;
		end_free = start_free + bytes2get;
		return chunk_alloc(size, nobjs);			//递归重新分配
	}
}

对于malloc失败的情况,即堆的空间不足以对内存池进行分配时,会对16个free list中剩余空间进行整合然后分配,若所有空间都已分配干净,那自然就会报错了。
————————————————————————————————————————————

可以说,在STL中空间适配器是一切容器的基础,而alloc配置器则是空间适配器的两大心脏之一,这里巧妙的运用了的内存管理手段来解决外部碎片的问题,并对一些特殊情况做出了合理的措施,还是很值得学习一个的。

参考资料:

猜你喜欢

转载自blog.csdn.net/qq_35713009/article/details/86603010
今日推荐