STL:空间配置器 —— 第二级配置器:__default_alloc_template | 自实现reallocate()

12 篇文章 1 订阅

前言

这一系列文章都是对STL的讲解,本文接着上篇第一级配置器,来讲解第二级配置器。第二级配置器是空间配置器的重点内容,也是相当麻烦的一部分内容。本文除了对《STL源码剖析》书中内容的讲解外,还自实现了 reallocate()函数,可以说相当不易,提前感谢大家的收看。
在这里插入图片描述

一、一二级空间配置器的关系

  1. SGI STL 一级配置器
template<int inst>
class __malloc_alloc_template {};

其中,allocate()直接使用 malloc(), deallocate()直接使用 free()。
模拟C++的 set_new_handler()以处理内存不足问题。

  1. SGI STL 二级配置器
template<bo0l threads, int inst>
class __default_alloc_template {};

其中:
2.1 维护16个自由链表( free lists ) ,负责16种小型区块的次配置能力。

内存池(memory pool) 以malloc() 配置而得。如果内存不足,转调用第一
级配置器(那儿有处理程序)。

2.2 如果需求区块大于128 bytes,就转调用第一级配置器。

二、一二级配置器的接口和运用方式

在这里插入图片描述

三、二级配置器——__default_alloc_template

第二级配置器多了一些机制,避免太多小额区块道成内存的碎片。小额区块带来的其实不仅是内存碎片,配置时的额外负担也是大问题。
额外负担永远无法避免,毕竟系统要靠这多出来的空间来管理内存。
但是区块愈小,额外负担所占的比例就愈大,愈显得浪费。

1.什么是小额区块

小额区块怎么理解?

在C程序向堆区使用malloc()申请空间时,除了指定获得的字节个数,还需要向系统申请越界位置。

示例:

#include<malloc.h>

int main(void)
{
	int* p = (int*)malloc(sizeof(int));

	free(p);
	p = NULL;

	return 0;
}

通过内存和监视器可以看出,这个申请的空间还有上越界和下越界标记,那么如果这种小额空间申请的次数越多,标记信息也越多,显得越浪费。
在这里插入图片描述

2. 维护自由链表

SGI 第二级配置器的做法是,如果区块够大,超过128 bytes时,就移交第一级配置器处理。 当区块小于128 bytes时,则以内存池( memory pool)管理,此法又称为次层配置( sub-allocation):每次配置一大块内存,并维护对应之自由链表(free-list )。下次若再有相同大小的内存需求,就直接从free-lists中拨出。如果客端释还小额区块,就由配置器回收到free-lists中 —— 是的,别忘了,配置器除了负责配置,也负责回收。为了方便管理,SGl 第二级配置器会主动将任何小额区块的内存需求量上调至8的倍数(例如客端要求30 bytes,就自动调整为32 bytes) ,并维护16个 free-lists,各自管理大小分别为8,16, 24,32,40,48,56,64,72,80,88,96,104,112,120,128 bytes的小额区块。free-lists的节点结构如下:

代码

这里的 free_list_link也可以理解为链表节点的next域

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

private:
	union obj
	{
		union obj* free_list_link;	// next;
		char client_data[1];
	};
	// 一个指针数组,自由链表
	static obj* volatile free_list[__NFREELISTS];

图示

那么该自由链表可以看作这样:

在这里插入图片描述

辅助函数ROUND_UP() / FREELIST_INDEX()

// 将bytes上调至8的倍数
static size_t ROUND_UP(size_t bytes) // 1~8 / 9~16...
{
	return (bytes + __ALIGN - 1) & ~(__ALIGN - 1);
}
// 根据区块大小,决定使用第n个free_list
static size_t FREELIST_INDEX(size_t bytes)
{
	return (bytes + __ALIGN - 1) / __ALIGN - 1;
}

3. 空间配置函数allocate()

此函数首先判断区块大小,大于128 bytes就调用第一级配置器,小于128 bytes就检查对应的free_list。如果 free_list 之内有可用的区块,就直接拿来用,如果没有可用区块,就将区块大小上调至8倍数边界,然后调用refill () ,准备为free list重新填充空间。refill()将于稍后介绍。

代码

public:
	static void* allocate(size_t size)
	{
		// 如果大于128bytes,交给第一级配置器
		if (size > (size_t)__MAX_BYTES)
		{
			return malloc_alloc::allocate(size);
		}
		
		obj* result = nullptr;
		obj* volatile* my_free_list = nullptr;
		my_free_list = free_list + FREELIST_INDEX(size);
		result = *my_free_list;
		if (nullptr == result)
		{
			void* r = refill(ROUND_UP(size));
			return r;
		}
		*my_free_list = result->free_list_link;
		return result;
	}

分析:假设size的大小为20.

  1. 如果申请的空间大于128字节,就交给第一级配置器。
  2. 声明一个结点类型的返回值result和一个二级指针my_free_list供后面使用。
  3. 根据size的大小计算出应该从free_list的哪个节点上去取空间。(free_list表示自由链表的首地址,size为20,那么就应该在17~24范围内的结点,即第三个结点(free_list + 2) 上去取空间。)
  4. 那么二级指针my_free_list指向需要去取空间的那个结点。
  5. 通过一级指针result取得二级指针的第一个结点,即第一个24字节的内存块的地址。
  6. 如果有内存块,就将其取出来,并将二级指针my_free_list(解引用后)指向result的next域(result->free_list_link)。
  7. 如果该结点没有内存块了,就需要交给填充函数refill()来实现,稍后会说到。
    在这里插入图片描述

4. 空间释放函数deallocate()

和allocate()相反,allocate()是链表的头删法,此函数相当于头插法。

该函数首先判断区块大小,大于128 bytes就调用第一级配置器,小于128 bytes就找出对应的free_list,将区块回收。这里就不多解释。

public:
	static void deallocate(void* p, size_t n)
	{
		if (n > (size_t)__MAX_BYTES)
		{
			return malloc_alloc::deallocate(p, n);
		}

		obj* q = (obj*)p;
		obj* volatile* my_free_list = nullptr;
		// 寻找相应的free_list
		my_free_list = free_list + FREELIST_INDEX(n);
		// 头插法,回收区块
		q->free_list_link = *my_free_list;
		*my_free_list = q;
	}

图示:
在这里插入图片描述

5. 内存池

一个从堆区申请的空间,heap_size表示该空间总大小;
start_free表示内存池起始位置,end_free表示内存池结束位置。

private:
	static char* start_free;
	static char* end_free;
	static size_t heap_size; // total
	
	// 配置一大块空间,可以容纳nobjs个size大小的区块
	// 如果配置nobjs个区块有所不便,nobjs会做出相应改变
	static char* chunk_alloc(size_t size, int& nobjs);
	
	// 返回一个大小为size的对象,
	// 并可能加入大小为size的其他区块到free_list中
	static void* refill(size_t size);

简图:

在这里插入图片描述

chunk_alloc()

该函数的工作是,从内存池中取出空间供给free_list使用

private:
	// 配置一大块空间,可以容纳nobjs个size大小的区块
	// 如果配置nobjs个区块有所不便,nobjs会做出相应改变
	static char* chunk_alloc(size_t size, int& nobjs)
	{
		char* result = nullptr;
		// 需要空间的总大小
		size_t total_bytes = size * nobjs;
		// 内存池剩余空间
		size_t bytes_left = end_free - start_free;	// 1

		// 如果内存池剩余空间满足需要空间
		if (bytes_left >= total_bytes)	// 2
		{
			result = start_free;
			start_free = start_free + total_bytes;
			return result;
		}
		else if (bytes_left >= size)	// 3
		{
			// 如果剩余空间只够1个以上的块(但小于总需求)
			nobjs = bytes_left / size;
			result = start_free;
			start_free = start_free + total_bytes;
			return result;
		}
		else
		{
			// 如果连一块空间都不够		// 4
			size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);
			
			// 将剩下的一点点空间再利用
			if (bytes_left > 0)			// 5
			{
				// 将剩余空间配置给合适的free_list
				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;
			}

			// 配置heap空间,补充内存池
			start_free = (char*)malloc(bytes_to_get);	// 6
			if (nullptr == start_free)				// 7
			{
				obj* volatile* my_free_list = nullptr;
				obj* p = nullptr;
				for (int i = size; i <= __MAX_BYTES; i += __ALIGN)
				{
					my_free_list = free_list + FREELIST_INDEX(i);
					p = *my_free_list;
					if (nullptr != 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;		// 8
				start_free = (char*)malloc_alloc::allocate(bytes_to_get);
			}

			// 修正内存池的结束位置和总大小	// 9
			end_free = start_free + bytes_to_get;
			heap_size += bytes_to_get;
			// 递归调用自己,修正nobjs
			return chunk_alloc(size, nobjs);
		}
	}

解析

根据代码中的数字来看

该函数的参数:size表示区块的大小(8的倍数),nobjs表示需要多少块(注意这里是引用)。

  1. 声明返回值、所需要空间总大小、内存池剩余容量并计算出来。
  2. 如果容量足够,就将返回值指向start_free,并将start_free下移(移到减去total_bytes的位置),返回result。
  3. 如果容量不足所需要的,但是够一个以上的区块,就将nobjs修正,只返回当前所能提供的块数,然后和第二步一样。
  4. 如果连一个区块也无法提供:先声明一个将要向堆区申请的变量(bytes_to_get);
  5. 如果内存池容量还大于0,就先将剩余的零头配置到合适的free_list中去(利用FREELIST_INDEX)。
  6. 然后配置堆区空间,申请大小是bytes_to_get,如果申请失败,说明堆区没有资源了;
  7. 如果堆区也没有资源,那就查看free_list中的空闲空间,利用for循环,利用二级指针遍历free_list,如果有空间就释放出当前free_list的第一个区块,并让内存池的首尾指针指向该区块(start_free和end_free);然后递归调用自身函数,用来修正nobjs。
  8. 如果出现意外,就是到了山穷水尽的时候,就调用第一级配置器的allocate(),会导致抛出异常,这个宏在第一级配置器的开始有定义;
  9. 如果配置堆区资源成功,即不执行6、7步骤,那么更新heap_size和end_free,并递归调用自己,修正nobjs。

refill()填充函数

再说之前的allocate(),如果free_list中没有区块了,就利用refill()从内存池中取区块来配置到free_list,这里默认取得20个区块,若不足20块,会配置的结点数可能小于20(这里的区块数就是chunk_alloc中的nobjs)。

private:
	static void* refill(size_t size)
	{
		int nobjs = 20;
		char* chunk = chunk_alloc(size, nobjs);	// 1
		if (1 == nobjs) return chunk;			// 2

		obj* volatile* my_free_list = nullptr;
		obj* result = (obj*)chunk;
		obj* current_obj = nullptr;
		obj* next_obj = nullptr;
		int i = 0;								// 3
		
		my_free_list = free_list + FREELIST_INDEX(size);	// 4
		*my_free_list = next_obj = (obj*)(chunk + size);	// 5
		for (i = 1; ; ++i)
		{
			current_obj = next_obj;							// 6
			next_obj = (obj*)((char*)next_obj + size);		// 7
			if (i == nobjs - 1)								//8
			{
				current_obj->free_list_link = nullptr;
				break;
			}
			current_obj->free_list_link = next_obj;			// 9
		}
		return result;
	}

解析

参数size表示需要填充的区块的大小(也用来寻找在哪个free_list上填充)

  1. 默认申请二十个区块,并利用指针指向当前申请的空间。
  2. 如果chunk_alloc修正后只有一个块nobjs,就先将这个块返回。
  3. 声明一些变量,分别是:用来指向free_list、指向申请的空间、指向当前区块,指向下一个区块、循环的参数 i 。
  4. 利用FREELIST_INDEX找到待填充的free_list。
  5. 接下来的步骤都是将这块大空间分成当前小区快串连起来,所以这里先将chunk偏移size个大小(因为第一块结点需要给客户使用),并强转成结点指针类型,让next_obj和*my_free_list都指向这个地址。
  6. 每次进入这个无限循环,都先让current_obj指向next_obj的位置。
  7. 将next_obj强转成char类型后加上size个大小(意味着往下偏移一个块大小),再强转成obj*类型指针,然后让next_obj指向这个偏移后的位置,意味着next_obj指向了下一个区块。
  8. 如果此时 循环条件 i 和 nobjs - 1 相等了,说明把所需的所有的块分割连接完成了,那么就将current_obj的 next域(free_list_link)置为nullptr。
  9. 这一步是连接操作,让当前结点的next域(free_list_link)指向 下一个结点(next_obj)。

内存池实际操作结果:
在这里插入图片描述

6. 自实现函数reallocate()

实际上该函数也是比较好实现的,调用reallocate()时会面临这些情况:

  1. old_sz > 128 && new_sz > 128;
    即原空间和新空间都大于128,就调用第一级配置器。

  2. old_sz 和 new_sz 都可以在同一个free_list上(比如 原:22字节,新:24字节;那么都在24字节这个free_list上取区块),那么可以不做处理,直接返回当前区块。

  3. 如果 old_sz > 128 && new_sz < 128;
    old_sz < 128 && new_sz > 128;
    old_sz < 128 && new_sz < 128;

接下来
围绕上面三点进行编写:

除去一二点,第三点可满足其他情况的发生具体流程是:

  1. 找出原大小和新大小哪个小;
  2. allocate()申请新空间;
  3. 将原空间的内容拷贝到新空间里,拷贝大小取决于原和新小的那个。
  4. deallocate()析构原空间。
  5. 返回新空间。
public:
	static void* reallocate(void* p, size_t old_sz, size_t new_sz)
	{
		// 1
		if (old_sz > (size_t)__MAX_BYTES && new_sz > (size_t)__MAX_BYTES)
		{
			return malloc_alloc::reallocate(p, old_sz, new_sz);
		}
		// 2
		if (ROUND_UP(old_sz) == ROUND_UP(new_sz))
		{
			return p;
		}
		// 3
		size_t sz = old_sz < new_sz ? old_sz : new_sz;
		void* s = allocate(new_sz);
		memmove(s, p, sz);
		deallocate(p, old_sz);
		return s;
	}

第二级配置器源码

// 第二级配置器
enum { __ALIGN = 8 };	// 小型区块的上调边界
enum { __MAX_BYTES = 128 };	// 小型区块的上限
enum { __NFREELISTS = __MAX_BYTES / __ALIGN };	// free_lists个数

template<bool threads, int inst>
class __default_alloc_template
{
private:
	// 链表
	union obj
	{
		union obj* free_list_link;	// next;
		char client_data[1];
	};

private:
	// 一个指针数组,自由链表
	static obj* volatile free_list[__NFREELISTS];

	static size_t ROUND_UP(size_t bytes) // 1~8 / 9~16...
	{
		return (bytes + __ALIGN - 1) & ~(__ALIGN - 1);
	}
	static size_t FREELIST_INDEX(size_t bytes)
	{
		return (bytes + __ALIGN - 1) / __ALIGN - 1;
	}

	static char* start_free;	// 内存池起始位置
	static char* end_free;		// 内存池结束位置
	static size_t heap_size; // total

	// 配置一大块空间,可以容纳nobjs个size大小的区块
	// 如果配置nobjs个区块有所不便,nobjs会做出相应改变
	static char* chunk_alloc(size_t size, int& nobjs)
	{
		char* result = nullptr;
		// 需要空间的总大小
		size_t total_bytes = size * nobjs;
		// 内存池剩余空间
		size_t bytes_left = end_free - start_free;

		// 如果内存池剩余空间满足需要空间
		if (bytes_left >= total_bytes)
		{
			result = start_free;
			start_free = start_free + total_bytes;
			return result;
		}
		else if (bytes_left >= size)
		{
			// 如果剩余空间只够1个以上的块(但小于总需求)
			nobjs = bytes_left / size;
			result = start_free;
			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)
			{
				// 将剩余空间配置给合适的free_list
				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;
			}

			// 配置heap空间,补充内存池
			start_free = (char*)malloc(bytes_to_get);
			if (nullptr == start_free)
			{
				obj* volatile* my_free_list = nullptr;
				obj* p = nullptr;
				for (int i = size; i <= __MAX_BYTES; i += __ALIGN)
				{
					my_free_list = free_list + FREELIST_INDEX(i);
					p = *my_free_list;
					if (nullptr != 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_alloc::allocate(bytes_to_get);
			}

			// 修正内存池的结束位置和总大小
			end_free = start_free + bytes_to_get;
			heap_size += bytes_to_get;
			// 递归调用自己,修正nobjs
			return chunk_alloc(size, nobjs);
		}
	}

	// 返回一个大小为size的对象,
	// 并可能加入大小为size的其他区块到free_list中
	static void* refill(size_t size)
	{
		int nobjs = 20;
		char* chunk = chunk_alloc(size, nobjs);
		if (1 == nobjs) return chunk;

		obj* volatile* my_free_list = nullptr;
		obj* result = (obj*)chunk;
		obj* current_obj = nullptr;
		obj* next_obj = nullptr;
		int i = 0;
		
		my_free_list = free_list + FREELIST_INDEX(size);
		*my_free_list = next_obj = (obj*)(chunk + size);
		for (i = 1; ; ++i)
		{
			current_obj = next_obj;
			next_obj = (obj*)((char*)next_obj + size);
			if (i == nobjs - 1)
			{
				current_obj->free_list_link = nullptr;
				break;
			}
			current_obj->free_list_link = next_obj;
		}
		return result;
	}

public:
	static void* allocate(size_t size)
	{
		if (size > (size_t)__MAX_BYTES)
		{
			return malloc_alloc::allocate(size);
		}
		
		obj* result = nullptr;
		obj* volatile* my_free_list = nullptr;
		my_free_list = free_list + FREELIST_INDEX(size);
		result = *my_free_list;
		if (nullptr == result)
		{
			void* r = refill(ROUND_UP(size));
			return r;
		}
		*my_free_list = result->free_list_link;
		return result;
	}

	static void deallocate(void* p, size_t n)
	{
		if (n > (size_t)__MAX_BYTES)
		{
			return malloc_alloc::deallocate(p, n);
		}

		obj* q = (obj*)p;
		obj* volatile* my_free_list = nullptr;
		// 寻找相应的free_list
		my_free_list = free_list + FREELIST_INDEX(n);
		// 头插法,回收区块
		q->free_list_link = *my_free_list;
		*my_free_list = q;
	}

	static void* reallocate(void* p, size_t old_sz, size_t new_sz)
	{
		if (old_sz > (size_t)__MAX_BYTES && new_sz > (size_t)__MAX_BYTES)
		{
			return malloc_alloc::reallocate(p, old_sz, new_sz);
		}
		if (ROUND_UP(old_sz) == ROUND_UP(new_sz))
		{
			return p;
		}

		size_t sz = old_sz < new_sz ? old_sz : new_sz;
		void* s = allocate(new_sz);
		memmove(s, p, sz);
		deallocate(p, old_sz);
		return s;
	}


};

// 对各个成员初始化
template<bool threads, int inst>
typename __default_alloc_template<threads, inst>::obj* volatile
__default_alloc_template<threads, inst>::free_list[__NFREELISTS] = {};

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;

//
//
// SGI STL
#ifdef __USE_MALLOC
typedef __malloc_alloc_template<0> malloc_alloc;
typedef malloc_alloc alloc;
#else
typedef __default_alloc_template<0, 0> alloc;
#endif

template<typename T, typename Alloc>
class simple_alloc
{
public:
	// 申请
	static T* allocate(size_t n) // n个T类型的
	{
		return Alloc::allocate(sizeof(T) * n);
	}
	static T* allocate()
	{
		return Alloc::allocate(sizeof(T));
	}

	// 删除函数
	void deallocate(T* p, size_t n)
	{
		if (nullptr == p) return;
		Alloc::deallocate(p, size(T) * n);
	}
	void deallocate(T* p)
	{
		if (nullptr == p) return;
		Alloc::deallocate(p, sizeof(T));
	}
};

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_索伦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值