STL源码剖析从零开始(空间配置器)


前言

本章主要介绍STL空间配置器最基本的内容,包括其中的基本接口,以及构造和析构工具construct和destroy的相关实现,空间配置以及释放相关的原理和函数,内存基本处理工具。

一、SGI特殊的空间配置器,std::alloc

1.概述

std::alloc是SGI使用的空间配置器,相较于alloc的std::allocator,alloc在效率上有长足的进步,文章从代码角度给出了解析,总体上alloc对这些情形进行了考虑:
1.向system heap要求空间
2.考虑多线程(multi-threads)状态 (暂时未考虑
3.考虑内存不足时的应变措施
4.考虑过多小型区块可能造成的内存碎片 (两级配置器
在看无敌的alloc之前先来看看弱鸡的allocator的相关接口吧

//这是allocator的内存分配函数,可以看出就是调用了::operatopr new来进行内存分配,且显然遇到问题没有想着解决而是直接进行报错
template <class T>
inline T* allocate(ptrdiff_t size, T*){
	set_new_handler(0);//这个函数是让operator new报错的时候返回0
	T* temp = (T*)(::operator new((size_t)(size* sizeof(T))));
	if(temp == 0){
		cerr << "out of memory" << endl;
		exit(1);
	}
	return temp;
}


template <class T>
inline void deallocate(T* buffer){
	::operator delete(buffer);//deallocator也仅仅是对::operator delete进行了一层封装罢了
}

allocator类中函数的实现也仅仅是对上述的接口进行调用封装,属实没有新意,自然效率上也会大打折扣

2.alloc详解

2.1alloc多级配置器的设计

为了解决小型区块造成的内存碎片问题,SGI设计了双层级配置器,当需求大的内存区块时,alloc直接调用第一级内存配置器,第一级配置器的底层实现是malloc和free,而当需求较小的区块(小于128bytes),采用第二级配置器且使用memory pool的整理方式。针对未开启_USE_MALLOC的情况我们来看看这种多级配置器是如何定义的!

typedef __malloc_alloc_template<0> malloc_alloc;
typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc;

__malloc_alloc_template<0>是第一级配置器
__default_alloc_template<__NODE_ALLOCATOR_THREADS, 0>是第二级配置器

接下来SGI对一二级配置器进行了封装,无论是内存的分配还是销毁,通过模板使对外暴露的接口都没有需要专门指定是由第一级还是第二级配置器来进行相关的工作

templata<class T, class Alloc>
class simple_alloc{
	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( n != 0) Alloc::deallocate(p, n * sizeof  (T));}
	static void deallocate(T *p)
			{Alloc::deallocate(p, sizeof (T));}
};

2.2第一级配置器剖析

第一级配置器实现了以下这些功能,处理内存不足的情况,分配内存,销毁内存。因为是一级配置器,内存分配和销毁是对malloc函数和free函数的调用,所以这里没有新东西,和allocator差不多捏。区别主要在于处理内存不足的情况!看看相关函数吧!

//omit some realize of somethings
//热知识,类似于void * foo()这是返回一个指针的函数
//void (*foo)()这是指针函数
private:
static void *oom_malloc(size_t);//类内静态函数的作用之一就是可以不用实例化也可以进行调用
static void *oom_realloc(void *, size_t);
static void (* __malloc_alloc_oom_handler)();

//都是函数指针
//默认malloc_alloc_oom_handler为0,待用户设定
template <int inst>
void (* __malloc_alloc_template<inst>::__malloc_alloc_oom_handler)() = 0;

//这里是内存重分配
template <int inst>
void * __malloc_alloc_template<inst>::oom_alloc(void *p, size_t n)
{
	void (*my_malloc_handler)();
	void *result;
	for(;;){
		my_alloc_handler = __malloc_alloc_oom_handler;//进行异常情况处理
		//如果__malloc_alloc_oom_handler为0那么代表没有设置处理函数,自然也就不会再次对内存进行处理
		//后续的内存再分配也无从谈起
		if(my_malloc_handler == 0) {__THROW_BAD_ALLOC;}
		(*my_malloc_handler)();//进行相关设置
		result = malloc(n);//重新分配
		if(result) return (result);//返回malloc出的地址
	}
}

oom_realloc的实现和oom_malloc相似,接下来要分析下SGI自己的set_new_handler,即set_malloc_handler

static void (* set_malloc_handler(void (*f)()))()
{
	void (* old)() = __malloc_alloc_oom_handler;
	__malloc_alloc_oom_handler = f;
	return (old);
}
//我们来解析一下这个函数
//static void (*set_malloc_handler(...)(){}
//这种带有实现的我们就认为他不是函数指针,而是返回函数指针的一个函数
//内部set_malloc_handler()的参数为 void(*f)(),也是一个函数指针,返回为空,参数为空

总结一下那就是,第一级配置器是对malloc和free函数进行了封装,并且加了对内存不足情况的处理,但是这个处理函数需要客端进行设置,不设置的话那就是河北省的元首–渣渣

2.2第二级配置器的相关分析

这一小节可以说是这一整章最为重要的部分,第二级配置器为了处理小的内存区块做了很多有趣的设计,可以来看看哦!
SGI第二级配置器的做法是将大于128bytes的内存请求移交给第一级配置器处理,小于128bytes的请求使用内存池管理。这种分类管理的方法又被称为次层配置(sub-allocation)。具体思路书中是这么介绍的:每次配置一大块内存,并维护对应之手自由链表(free-list),下次弱再有相同大小的内存需求,就直接从free-list中拔出,如果客端释放小额区块,就由free-list负责返还。同时为了管理方便,第二级配置器会主动将小额区块的需求量调整至8的倍数。注意,freelist中有不同的指针分别指向维护8,16,24…区块大小的指针
说了这么多,看看free-list的实现是如何的吧

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

通过对union的使用,我们可以省去链表头的开销,因为一旦一段区域的内存被使用,因为union的性质,前面的obj指针将不会占用空间,而对于没有使用的内存,client_data这个是没有使用的,free-list可以通过free_list_link来获取。
接下来看看第二级配置器的一些简单内容!

enum {__ALIGN = 8};
enum {__MAX_BYTES = 128};
enum {__NFREELISTS = __MAX_BYTES/__ALIGN};

template <bool threads, int inst>
class __default_alloc_template{
private:
	static size_t ROUND_UP(size_t bytes){
		return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1));//这里解释一下,因为ALIGN为8,ALIGN - 1 = 7即b00000111,取反为b11111000,
		//bytes + 7会使非8bytes大小整数倍的bytes值整体在二进制第四位进1,在和前面的b11111000进行与运算,就可以得到结果向上roundup到8倍数的效果了
		//推而广之,我想要让一个数变成2^x整数倍都可以采取这样的方法
	}
}
private:
	static obj * volatile free_list[__NFREELISTS];
	static size_t FREELIST_INDEX(size_t bytes){
		return (((bytes) + __ALIGN - 1) / __ALIGN - 1); //加上ALIGN - 1的目的是让0-7之间的数能归到1,再-1后不会出现负数的情况
	}

上述代码完成了roundup工作,以及freelist的分配的工作。但我们还没有触及第二级分配器的核心,也就是他的内存池设计,即前文说到的次配置能力。

2.2.1空间配置函数allocate

第二级配置器有一个接口allocate,他对每次进行的内存申请进行判断,对大于128bytes的调用第一级配置器,对于小于等于128bytes的则调用第二级配置器相关的接口。当使用第二级配置器相关接口的时候,先判断有没有可用的区块,没有则将区块大小进行调整,然哦户refill重新填充空间(上述为书上原话,有些难以理解,接下来对着代码瞅瞅!)

static void * allocate(size_t n){
	obj * volatile * my_free_list;//volatile是为了处理多线程的情况
	obj * result;
	if(n > (size_t) __MAX_BYTES){
		return (malloc_alloc::allocate(n));
	}
	my_free_list = free_list + FREELIST_INDEX(n);//free_list是指向8bytes大小空间的,而index从0开始,满8bytes进1
	//所以index对应了freellist指向空间的序号,具体可以看看书中给出的图
	result = *my_free_list;//这个数据结构类似于链式前向星,所以my_free_list指向地址的内容为相应区块的地址
	if(result == 0){//这个链式前向星的结尾应该是0,当用完可用的后剩下的就只剩尾巴0了
		void *r - refill(ROUND_UP(n));
		return r;
	}
	*my_free_list = result -> free_list_link;
	return (result); 
}

在这里插入图片描述

这张图可以说是把freelist整体的一个结构解析得相当清晰明了了!

2.2.2重新填充refill()

看完上面的代码,其实它的整体思想很好理解,但是里面有一个细节是还未说到的,就是在result为0的情况下进行了重新填充refill(ROUND_UP(n)),看看这个小可爱函数具体怎么实现的吧,即将走入本节的深水区,前方高能哦

template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n){
	int nobjs = 20; //qks看到bjs瞬间dna动了QAQ,fkhr死个马先
	//chunk的中文意思是一大块
	//nobjs代表需要分配的块的数量
	//补充一下chunk_alloc的定义
	//static char *chunk_alloc(size_t size, int &nobjs);这里nobjs是引用传值哦,为了能够自适应不足20的情况
	char * chunk = chunk_alloc(n, nobjs);
	obj * volatile * my_free_list;
	obj * result;
	obj * current_obj, * next_obj;
	int i;
	if(nobjs == 1) return chunk;
	my_free_list = free_list + FREELIST_INDEX(n);//分配完毕,转移到所需要大小的头指针那,注意这里的n是roundup过的
	result = (obj *)chunk;
	*my_free_list = next_obj = (obj *)(chunk + n);//移向下一个位置
	for(i = 1; ; i++){//这个循环的作用就是将分配的没用的用指针串起来
		current_obj = next_obj;
		next_obj = (obj *)((char *)next_obj + n);
		if(nobjs - 1 == i){//因为分配了1个所以剩下还剩nobjs - 1个捏
			current_obj -> free_list_link = 0;//从这也可以看到这个链表的尾指针是0
			break;
		} else {
			current_obj -> free_list_link = next_obj;
		}
	}
	return (result);
}

我们离真相又进了一步,通过上面的代码我们可以知道refill大概干了这么些事,首先是每次refill都是默认申请20个所需求大小的块,然后进入内存池中进行申请,返回的是第一个内存块地址,后续的内存块地址要用链表存起来。但是我们层层深扒的时候会发现里面又有了一个新的未知函数,chunk_alloc,但是我们可以知道他返回值是一个指向char的指针。书中后面一节就紧接着介绍了chunk_alloc函数,我们继续来看看把

__default_alloc_template<threads, 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;//通过引用能够让进行调用的函数知道最终分配了多少块
		total_bytes = size * nobjs;//更新总的需求
		result = start_free;
		start_free == total_bytes;//更新内存池
		return (result);
	} else{//如果一个都满足不了
	//heap_size在前文中有如下定义
	//size_t __default_alloc_template<threads, inst>::heap_size = 0;
	//而代码最后有heap_size += bytes_to_get表示这个值会一次次增加
	//总之就是想要向系统申请一大块内存
		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;//链式前向星经典insert写法,不能理解的看看acwingY总的算法课
			*my_free_list = (obj *)start_free;//
		}
		//塞完了就开始分配
		start_free = (char *)malloc(bytes_to_get);
		if(start_free == 0){//如果没有分配到内容,那么进行相应的处理
			int i;
			obj * volatile * my_free_list, *p;
			for(i = size; i <= __MAX_BYTES; i += __ALIGN){
				my_free_list = free_list + FREELIST_INDEX(i);//从当前size开始,逐渐向更大的区块寻找让他们向下兼容的机会
				p = *my_free_list;//将p指向my_free_list
				if(p != 0){		//如果有空间,那么就搞一遍链式前向星的插入,大家自己也可以脑子里过一过这个过程
					*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);//这边的处理比较奇怪,看看就够了
		}
	heap_size += bytes_to_get;//如果是正常分配,那么这里会更新heapsize
	end_free = start_free + bytes_to_get;//内存池的尾
	return (chunk_alloc(size, nobjs));
}

总结一下,我们的chunk_alloc做的事情大概有如下几点:在有空余情况下直接进行分配,同时更新内存池。内存池不太足那么能分配几个是几个。如果真的一滴都没有了,那么先把内存池中剩下的量先分配出去然后再向系统申请。如果申请失败,那么就奢侈一把,将其他freelist中的内容变成内存池的内容。

3.构造和析构基本工具construct和destroy

我们知道我们在new一个类的时候,其实是发生了内存的分配和构造两个部分的,我们看看SGI STL是怎么实现构造这一部分的吧

#inlcude <new.h>
template <class T1, class T2>
inline void construct(T1 *p, const T2& value){
	new(p) T1(value);//注意这里没有写错,就是这么一个写法,叫做placement new语法,目的是在指定内存构造内容
}

template <class T>
inline void destroy(T* pointer){
	pointer-> ~T();//调用析构函数
}

template <class ForwardIterator>
inline void destroy(ForwardIterator first, ForwardIterator last){
	__destroy(first, last, value_type(first);
}

template <class ForwardIterator, class T>{
	typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;
	__destroy_aux(first, last, trivial_destructor());
}

template <class ForwardIterator>
inline void 
__destroy_aux(ForwardIterator first, ForwardIterator last, __false_type){
	for( ; first < last; ++first)
		destroy(&*first);//调用析构函数
}

template <class ForwardIterator>
inline void __destroy_aux(ForwardIterator, ForwardIterator, __true_type){};//如果是pod则不需要手动干掉他

inline void destroy(char *, char *){}
inline void destroy(wchar_t*, wchar_t *) {}

这里和上面一节相比十分简单,就干了两件事,destroy的时候看看是不是POD,是的话就不用进行手动控制,不是的话就需要手动调用析构函数。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值