[SGI STL]空间配置器--内存管理

[SGI STL]系列文章前言

       废话不多说,读侯捷的SGI STL源码分析目的有三个:

1,接触c++不久就开始跟STL打交道,一直有个好奇心,这么强大的库到底是谁、咋实现的?;

2,不熟悉实现就用不好STL,所以想更好的应用STL,就有必要一探其底层驱动;

3,引用林语堂先生的一句话:“只用一样东西,不明白它的道理,实在不高明”;


目录

1,如何使用空间适配器

2,一个标准的空间配置器

3,SGI STL 空间配置器架构

4,构造和析构的基本工具:construct()和destroy()

5,空间的配置与释放,alloc

5.1 第一级配置器 __malloc_alloc_template

5.2 第二级配置器 __default_alloc_template

5.2.1 freelist

5.2.2 二级配置器中的空间配置函数allocator()

5.2.3 空间释放函数deallocate()

5.2.4 内存池(mem pool)

5.2.5 第二级配置器总的流程框图

6 内存基本处理工具

7 小结


1,如何使用空间适配器

其实以运用STL的角度来看,完全可以忽略空间适配器,因为每个容器都是通过默认参数指定好了allocator,通过查看vector的声明可以看出:

template<class _Ty,class _Alloc = allocator<_Ty> >
	class vector
{
//...
}

如下代码中的vector没有指定allocator,默认的allocator会自动根据你传入的元素,调整内存空间:

#include <vector>

void main()
{
	std::vector<int> vecTemp;
	for (int i = 0;i<10;i++)
	{
		vecTemp.push_back(i);
	}

	getchar();
}

其实,完整的vecTemp声明应该是 vetor<int, allocator<int>> vecTemp。

假如我们自定义了将内存分配指向磁盘或者其他存储介质空间的allocator,那么只要在声明时传入设计好的allocator,不再使用默认的allocator就行了。

那么问题来了,怎么样才能设计一个allocator呢?继续看~

2,一个标准的空间配置器

首先,设计一个空间配置器需要包含什么接口呢?我们从如下的例子引入:

	class Foo{...};
	Foo* pFoo = new Foo;//< 第一阶段,干了俩事:1,配置内存 2,在配置好的内存上构造对象
	delete pFoo; //< 第二阶段,也干了俩事:1,析构对象 2,释放内存

所以,一个allocator至少要包含四个功能:申请内存、构造对象、析构对象、释放内存。

其次,我可以很负责人的告诉你,如果你的allocator只包含上述四个功能,肯定无法再STL中运用^_^。因为STL对allocator的组成已经规定好了,即STL规范。那么STL中的allocator相关的规范是啥呢?我们通过一个符合STL标准的allocator(主要参考书中的JJ::allocator,略有修改)来说明:

namespace JJ 
{
	template <class T>
	class allocator
	{
	public:
		//< 七个typedef主要是为了迭代器的类型萃取,迭代器章节会提到
		typedef T		value_type;
		typedef T*		pointer;
		typedef const T*	const_pointer;
		typedef T&		reference;
		typedef const T&	const_reference;
		typedef size_t		size_type;
		typedef ptrdiff_t	difference_type;

		//< 成员模板 rebind
		//< 定义了一个associated type other,other也是一个allocator的实例,但是负责管理的对象类型与T不同
		//< 具体可以参考https://blog.csdn.net/qq100440110/article/details/50198789
		template <class U>
		struct rebind
		{
			typedef allocator<U> other;
		};

		//内存申请 直接使用new
		pointer allocate(size_type n, const void* hint = 0)
		{
			T* tmp = (T*)(::operator new((size_t)(n * sizeof(T))));
			if (tmp == 0)
				cerr << "out of memory" << endl;

			return tmp;
		}

		//构造函数 使用placement_new 在p处构造T1
		void construct(pointer p, const T& value)
		{
			new(p) T1(value);
		}

		//析构函数
		void destroy(pointer p)
		{
			p->~T();
		}

		//释放内存  直接使用delete
		void deallocate(pointer p)
		{
			::operator delete(p);
		}

		//取地址
		pointer address(reference x)
		{
			return (pointer)&x;
		}

		//返回const对象的地址
		const_pointer const_address(const_reference x)
		{
			return (const_pointer)&x;
		}

		//可成功配置的最大量
		size_type max_size() const
		{
			return size_type(UINT_MAX / sizeof(T));
		}
	};
}// NAMESPACE_JJ_END

这样,我们设计的第一个allocator完成了,就可以在实际中使用了:

	int ia[5] = { 1,2,3,4,5 };
	vector<int, JJ::allocator<int> > vec(ia, ia + 5);

3,SGI STL 空间配置器架构

有人可能会想,既然设计一个空间配置器这么简单,STL的多个毛啊,为啥它的这么NB。其实,STL的空间配置器不只多个毛,是多很多毛,不是NB,而是很NB,从这就能看出来王者与青铜的差别了,膜拜之~

由于一个内存配置与释放操作通常分两个阶段(见2中的例子),为了精密分工,STL allocator将这两个阶段的操作区分开来:

1,内存配置由alloc::allocate()负责,内存释放由alloc::deallocate()负责;

2,对象构造由::construct()负责,对象析构由::destroy()负责。

其实,对于内存配置和释放还有个allocator::allocate()和allocator::deallocate(),这是SGI定义的符合部分STL标准的配置器,但由于效率不佳,不推荐使用。其实它就是对::operator new和::operator delete做了一层薄薄的封装。

思维导图如下:

书中原图:

毋庸置疑,<stl_construt.h>和<stl_alloc.h>是STL空间配置器的重头菜,我们在这里分别介绍。

4,构造和析构的基本工具:construct()和destroy()

先上一张书中的construt()和destroy()示意图,对照着图就很容易理解了:

首先,对于construct()来说很简单了,就是接受一个指针p和一个初值value,用途就是将初值设定到指针所致的空间上,可以通过placement new来完成。

template<class T1, class T2>
void construct(T1 *p, const T2 &value)
{
	new(p) T1(value);
}

其次,从图中可以看出destroy()有两个版本:

第一个版本:接受一个指针(图中的第四个),准备将所指之物析构掉。这很简单,直接调用析构函数即可。

template<class T>
void destroy(T *ptr)
{
	ptr->~T();
}

第二个版本:接受一个迭代器区间,准备将这个范围内的对象析构掉。

再讲这个版本的destroy()之前,讲一下trivial destructor:如果不定义析构函数,而是使用系统自带的,也就是析构函数没什么作用,那么这个析构函数称为trivial destructor。

这里提现了STL作者的设计亮点,他不是直接调用每个对象的析构,而是首先确定每个对象是否有non_rivial destructor(即自己定义了析构函数)。如果有,则调用对象析构,如果没有,就什么也不做结束。

反正思路就是上面写的,具体可以看一下书上的代码,至于每个对象是否有non_rivial destructor的判断,则用到了_type_traits<T>,会在后面讲述。

图中的第二个和第三个是第二个版本的char*和wchar*的特化。

以上就是关于construt()和destroy()的所有内容,其实还是挺简单的。

5,空间的配置与释放,alloc

这一节我觉得是整个SGI STL空间配置器的核心。

设计者设计了两个配置器,准确的说是两级配置器,两个配置器相辅相成,相互配合最终完成空间的配置。

第一级配置器直接使用malloc()和free(),第二级则视情况采取不同的策略。而分界点是配置的内存是否大于128B,大于就用第一级,小于等于则通过第二级访问复杂的memory pool整理方式。

通过是否定义_USE_MALLOC宏,来设定是只打开第一级还是同时打开第一级与第二级。SGI STL没定义那个宏,也就是同时开放一、二级。

5.1 第一级配置器 __malloc_alloc_template

先说一下整体的思路。就像上图所说的,这个配置器中的allocator()直接调用C中的malloc(),reallocator()直接调用C中的realloc()。如果配置成功,则返回指针,如果不成功则调用out of memory处理;deallocator()直接调用free()。

out of memory主要调用用户设置的__malloc_alloc_oom_handler,这个可以通过模拟C++中的set_new_handler()的set_malloc_handler()来设定。如果用户指定了,则循环调用这个handler,直到分配到内存,如果没定义,则抛bad_alloc异常。

具体代码如下:

#if 0
#include<new>
#define __THROW_BAD_ALLOC throw bad_alloc
#elif !defined(__THROW_BAD_ALLOC)
//#include<iostream.h>
#define __THROW_BAD_ALLOC cout<<"Out Of Memory."<<endl; exit(1)
#endif

//inst完全没用
template<int inst>
class __malloc_alloc_template
{
private:
	//以下用来处理内存不足的情况;oom:out of memory
	static void * oom_malloc(size_t n); 
	static void * oom_realloc(void *p, size_t n);
	static void(*__malloc_alloc_oom_handler)();

public:
	static void* allocate(size_t n)
	{
		void *result = malloc(n); //< 直接调用malloc()
		if (result == 0)
			result = oom_malloc(n); //< 分配失败调用oom_malloc()
		return result;
	}

	static void  deallocate(void *p, size_t)
	{
		free(p); //< 直接调用free()
	}

	static void* reallocate(void *p, size_t old_sz, size_t new_sz)
	{
		void *result = realloc(p, new_sz); //< 直接调用C中的realloc()
		if (0 == result)
			result = oom_realloc(p, new_sz);  //< 分配失败调用oom_realloc
		return result;
	}

	//模拟C++中的set_new_handler(),也就是通过这个函数指针来指定自己的out-of-memory操作
	static void(* set_malloc_handler(void(*f)()))()
};

// 初值为0,客户端指定
template<int inst>
void(*__malloc_alloc_template<inst>::__malloc_alloc_oom_handler)() = 0;

//如果指定了 __malloc_alloc_oom_handler,则循环调用,直到分配到内存,否则抛异常
template<int inst>
void* __malloc_alloc_template<inst>::oom_malloc(size_t n)
{
	void(*my_malloc_handler)();
	void *result;
	for (;;)
	{
		my_malloc_handler = __malloc_alloc_oom_handler;
		if (0 == my_malloc_handler)
		{
			__THROW_BAD_ALLOC;
		}
		(*my_malloc_handler)();
		result = malloc(n);
		if (result)
			return result;
	}
}

//如果指定了 __malloc_alloc_oom_handler,则循环调用,直到分配到内存,否则抛异常
template<int inst>
void* __malloc_alloc_template<inst>::oom_realloc(void *p, size_t n)
{
	void(*my_malloc_handler)();
	void *result;
	for (;;)
	{
		my_malloc_handler = __malloc_alloc_oom_handler;
		if (0 == my_malloc_handler)
		{
			__THROW_BAD_ALLOC;
		}
		(*my_malloc_handler)();
		result = realloc(p, n);
		if (result)
			return result;
	}
}

所谓的C++ new handler机制是指,你可以要求系统在内存配置需求无法被满足时,调用一个你指定的函数。之所以要模拟这种机制,因为它并不是使用::operator new来配置内存的。

5.2 第二级配置器 __default_alloc_template

其实第一级配置器可以说用户是通过new和free直接与系统内存打交道的,而第二级配置器相对比较复杂,大概分为3块内存,简要的沟通机制可参考下图:

三块空间分别为freelist、mempool、系统内存。

各实现的伪代码可以参考博客:https://blog.csdn.net/qq973177663/article/details/50815055?locationNum=9

总是通过freelist来获得内存,freelist如果没有内存了,则调用refill()向mempoor获得内存,mempoor如果也不够,则调用trunk_alloc()向内存申请,内存都没有调用第一级配置器,看看out of memory机制能够起作用。

整个第二级配置器无非就是对上述freelist空间、mempoor空间的创建、内存申请、内存回收、以及之间的通信。源码如下:

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

template<bool threads, int inst>
class __default_alloc_template
{
public:
	//< 三个接口
	static void *allocate(size_t n);
	static void deallocate(void *p, size_t n);
	static void* reallocate(void *p, size_t old_sz, size_t new_sz);

private:
	// 将申请的size上调至__ALIGN的倍数
	static size_t ROUND_UP(size_t bytes)/
	{
		return (bytes + __ALIGN - 1) & ~(__ALIGN - 1);//
	}
	
	// freelist节点结构
	union obj
	{
		union obj * free_list_link;
		char client_data[1];
	};

	// 根据要申请的区块大小,决定使用第n号freelist,n从0算起
	static size_t FREELIST_INDEX(size_t bytes)
	{
		return (bytes + __ALIGN - 1) / __ALIGN - 1;
	}

	// 返回一个大小为n的区块对象,并可能(通常)加入大小为n的其他区块到freelist
	static void* refill(size_t n);

	// 配置一大块空间,可容纳nobjs个大小为size的区块
	// 注意此处nobjs是引用,如果配置有所不便(内存不够),nobjs会降低
	static char* chunk_alloc(size_t size, int &nobjs);

	static obj * free_list[__NFREELISTS]; //< 16个freelist 
	static char *start_free;//< 内存池其实位置
	static char *end_free; //< 内存池结束位置
	static size_t heap_size; //< 配置内存的附加量
};

// 赋初值
template<bool threads, int inst>
char* __default_alloc_template<threads, inst>::start_free = 0;
template<bool threads, int inst>
char* __default_alloc_template<threads, inst>::end_free = 0;
template<bool threads, int inst>
size_t __default_alloc_template<threads, inst>::heap_size = 0;

template<bool threads, int inst>
__default_alloc_template<threads, inst>::obj*
__default_alloc_template<threads, inst>::free_list[__NFREELISTS] =
{ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 };

 

5.2.1 freelist

简单说,就是16个8byte倍数但小于128byte的链表,链表的节点如下:

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

其实我对这个节点的定义还是有点疑问的,具体请看我的另一篇博客:

https://blog.csdn.net/u012481976/article/details/82724916

一共16个freelist,每个freelist就是一个链表,16个的区别就是链表节点所占空间大小不一样,:

对于每个freelist的空间申请与释放,其实就是一些链表的操作。

5.2.2 二级配置器中的空间配置函数allocator()

以下代码描述了如何利用二级配置器中的allocator()配置空间,以及freelist空间如何与mempool之间通信,代码如下:

static void* allocate(size_t n)
{
	obj* volatile* my_free_list;
	void* result = 0;

	//如果大于128B, 直接调用一级配置器
	if (n > (size_t)_MAX_BYTES) 
	{
		return (malloc_alloc::allocate(n));
	}
	//寻找 16个free-list 中的一个
	my_free_list = free_list + FREELIST_INDEX(n);
	result = *__my_free_list;
	if (result == 0)
	{
		//如果freelist上没有可用空间,则将空间调整至8的倍数
        //调用refill,向mempool申请内存,重新填充该freelist
		result = refill(ROUND_UP(n));
		return result;
	}
	else 
	{
		*my_free_list = result->_M_free_list_link;
	}

	return result;
};

其中freelist与mempool之间的通信函数refill(),源码如下:

template<bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n)
{
	//默认取20个新节点连接到freelist上(其实是19个,第一个返回给用户)
	int nobjs = 20;

	//调用chunk_alloc(),尝试取得nobjs个区块作为freelist的新节点
	//注意此处参数nobjs是通过引用传入,有可能变小
	char* chunk = chunk_alloc(n, nobjs);
	obj* volatile* my_free_list;
	obj * result;
	obj * current_obj, *next_obj;
	int i;

	//如果只获得一个区块,则将这个区块直接反馈,freelist无新节点
	if (1 == nobjs)
	{
		return chunk;
	}

	//找到需要填充的链表的位置
	my_free_list = freeList + FREELIST_INDEX(n);
	result = (obj*)chunk;//第一块返回给客户端
	//引导freelist指向新的空间
	*my_free_list = next_obj = (obj*)(chunk + n);//这里把第二块先挂到指针数组对应位置下  //注意这里的n在传参数时已经调整到8的倍数
	for (i = 1;; i++) {//从1开始,0返回给客户端
		cur_obj = next_obj;
		next_obj = (obj*)((chat*)next_obj + n);
		if (nobjs - 1 == i) {                   //因为第一次从内存池取下的空间在物理上是连续的 尾插方便用 以后用完还回自由链表的就不是了
			cur_obj->free_list_link = NULL;//这里没有添加节点
			break;
		}
		else {
			cur_obj->free_list_link = next_obj;//nobjs - 2是最后一次添加节点
		}
	}
	return result;
}

 

5.2.3 空间释放函数deallocate()

如果释放的空间大于128b则调用第一级配置器,如果小于128b,则将要释放的空间链接到对应的freelist上,也就是一个在链表头插入节点的过程:

static void deallocate(void* p, size_t n)
{
	obj* volatile*  my_free_list;
	obj* q = (obj*)p;

	//如果大于128,调用第一级配置器
	if (n > (size_t)_MAX_BYTES)
	{
		malloc_alloc::deallocate(p, n);
		return;
	}	
		
	//寻找对应的freelist
	my_free_list = _S_free_list + _S_freelist_index(n);
	//回收该区块
	q->free_list_link = *my_free_list;
	*my_free_list = q;
}

5.2.4 内存池(mem pool)

chunk_alloc()是负责mem pool与系统内存打交道的,源码如下:

template<bool threads, int inst>
void* __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 
	{
		// 内存池剩余空间连一个区块的大小都无法提供
		size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);
		// 以下试着让内存池中的残余零头还有利用价值
		if (bytes_left > 0) 
		{
			// 内存池内还有一些零头,先配给适当的free list
			// 首先寻找适当的free list
			obj * volatile * my_free_list = free_list + FREELIST_INDEX(bytes_left);
			// 调整free list,将内存池中的残余空间编入
			((obj *)start_free)->free_list_link = *my_free_list;
			*my_free_list = (obj *)start_free;
		}

		// 配置heap空间,用来补充内存池
		start_free = (char *)malloc(bytes_to_get);
		if (0 == start_free) 
		{
			// heap空间不足,malloc失败
			int i;
			obj * volatile * my_free_list, *p;
			// 试着检视我们手上拥有的东西,这不会造成伤害。我们不打算尝试配置
			// 较小的区块,因为那在多进程机器上容器导致灾难
			// 以下搜寻适当的free list
			// 所谓适当是指“尚未用区块,且区块够大”的free list
			for (i = size; i <= __MAX_BYTES; i += __ALIGN) 
			{
				my_free_list = free_list + FREELIST_INDEX(i);
				p = *my_free_list;
				if (0 != p) 
				{ // free list内尚有未用块
							  // 调整free list以释放未用区块
					*my_free_list = p->free_list_link;
					start_free = (char *)p;
					end_free = start_free + i;
					// 递归调用自己,为了修正nobjs
					return chunk_alloc(size, nobjs);
					// 注意,任何残余零头终将被编入适当的free list中备用
				}
			}
			end_free = 0; // 如果出现意外,调用第一级配置器,看看oom机制能否尽力
			start_free = (char *)malloc_alloc::allocate(bytes_to_get);
			// 这会抛出异常 或 内存不足的情况得到改善
		}
		heap_size += bytes_to_get;
		end_free = start_free + bytes_to_get;
		// 递归调用自己,为了修正nobjs
		return chunk_alloc(size, nobjs);
	}
}

我觉得书上举的例子对这段代码的解释再合适不过了,非常透彻:

5.2.5 第二级配置器总的流程框图

自己懒得画了摘了一个:

6 内存基本处理工具

提供的三个工具uninitialized_copy()、uninitialized_fill()、uninitialized_fill_n(),用于将内存的配置与对象的构造分别开来。如何分开的呢?我们先看一下对于一个全区间的构造函数,如何构造对象的:

1,配置内存区块,足以包含范围内的所有元素;

2,在内存上构造对象;

那么这三个函数是如何发挥作用的呢?这里用到了is_POD_type的概念。POD意指Plain Old Data,也就是标量型别或传统的C struct型别。POD必然拥有trivial ctor/dtor/copy/assignment函数,因此我们可以:

对POD型别采取最有效的初值填写法,如:

int a;
a = 5;

而对non-POD型别采取最保险的安全做法:

char* p = new char;
new(p) char(5);

至于怎么判断一个迭代器所指对象的型别,那就是利用__type_trait了,后续再说。

如果is_POD_type是__true_type,那么这几个工具就调用相应的算法copy()、fill()、fill_n()。如果是__false_type则调用第4节提到的construct()。

7 小结

花了三天晚上看书,加上一个周末的下午+晚上串联思想与写这篇博客,总体感觉收获还是蛮多的,对于STL的内存配置以及泛型变成都有了一定得了解,还可以~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值