C++-----MySTL实现(5)---空间配置器实现(alloc)

1、相关

        前面实现的四部分其实都是为这一章做准备。首先我们来看,STL一共有六大组件:容器、算法、迭代器、仿函数、配接器、配置器。前面迭代器的实现其实只是定义了一些标准,不是真正的迭代器实现,真正的迭代器实现会放在每一个容器里,而容器的实现需要一个基本的功能就是内存分配。这一部分就来实现空间配置器allocator。

        在STL的源码里,allocator这个分配器其实很简单只是对C++的operator new(new其实调用的就是这个)做了一个简单的封装。而在这里我们实现的STL中一个特殊的空间配置器,alloc。

        在平时编程中如果要构造一个对象和析构一个对象,进行的操作其实会分为两部分,构造一个对象,会先配置内存,然后构造对象,而析构一个对象会先析构对象,再将内存释放。在分配器的实现上,我们将这两部分分开,内存分配由alloc::allocate()负责,内存释放用alloc::deallocate(),而对象的构造和析构用::construct()和::destroy()负责,这一部分在前面已经实现。

    

        如上图,在前面我们已经实现了construct.h和uninitialized.h。construct.h里完成了对象的构造行为和对象析构行为,那么在这一部分就来实现内存的配置和释放。

        对象构造前的空间配置和对象析构后的空间释放,都由alloc.h负责,SGI对空间配置器的设计哲如下:1)向系统堆要求空间。2)考虑多线程的状态。3)考虑内存不足时的应变措施。4)考虑过多小型区块可能造成的内存碎片问题。C++的内存基本操作是::operator new(),释放内存的基本操作是::operator delete()。其实在在他们的内部还是使用C语言中的malloc和free实现的。小型区块的内存破碎问题原因如下:

        当你使用malloc申请一个内存时,系统会分配给你如上一段内存,中间的size是你申请的大小,所以如果每次申请内存都使用malloc的话会造成很多的不必要的浪费。所以在STL中设计了双层级配置器。第一层直接使用malloc()和free(),第二层则视情况采取不同的策略:当配置区块内存大于128bytes,视为足够大,调用一级配置器。当小于时,为了降低额外负担便采用内存池的方式。

        二级配置器的做法:如果要分配的内存大于128字节就调用一级配置器小于128时,以内存池管理。具体做法是:每次配置一块大的内存,并维护对应的自由链表,下次若再有相同大小的内存需求直接从自由链表里拨出如果客端释还小额区块由配置器会受到自由链表,为了方便管理,第二级分配器会主动将任何小额区块的内存需求上调至8的倍数,并维护16个自由链表,各自管理的大小分别是8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128字节的小额区块。自由链表的节点结构如下:

union obj                            //free list节点
{
    union obj* free_list_link;
	char client_data[1];    /* The client sees this. */
};

        维护链表每个节点需要额外的指针,指向下一个节点,那么这不又是一种负担嘛,上面的节点用的union,由于union的原因,从第一个字段来看,obj可以看做一个指针,指向另一个obj。从第二个字段看可以视为一个指针,指向实际的区块,使得一物二用。

        二级配置器一开始第一个函数是用来将需要的内存上调至8的倍数,而这个是一个很巧妙的设计,代码如下:

/*一个数k如果正好是2^n的倍数,那么用二进制来 表示的话,
			*低n位必定全是0,而 _ALIGN-1的二进制表示则是低n位全为1,
			*那么k+ _ALIGN-1和~(_ALIGN – 1)取与必定是k本身;
			*如果一个数k不是2^n的倍数,则必有m*2^n<k<(m+1)*2^n,
			*要做的就是求出(m+1)*2^n, 则k用二进制来表示的话,低n位
			*肯定不全为0,再加上_ALIGN-1必然会向第n+1位进位,此时若
			*将低n位全部置零的话,得到刚好就是 (m+1)*2^n,而
			*和~(_ALIGN – 1)取与正好达到这一效果。
            *这里的关键在于,对一个数如果有m*2^n<k<(m+1)*2^n,
			*那么有 k=m*2^n+p, 且p<2^n,这样k就分成了两部分,
			*在用二进制表示的时候,p是存储在低n位中,
			*而m*2^n是存储在高位中(即除去低n位的剩余位数),
			*P+ALIGN-1,肯定向高位进一,即为(m+1)*2^n+P1,
			*然后与全0,则得到(m+1)*2^n=2^(n+1)。
*/

static size_t ROUND_UP(size_t bytes)    
		{
			//这段代码是常用的内存调整的常用工具,将输入bytes调整到ALLGN的倍数
			
			return (((bytes)+_ALIGN - 1) & ~(_ALIGN - 1));
		}

        具体的解析上面已经解释。

2、代码

/*
* 一、二级配置器,并提供了
* 接口类simple_alloc
*/

#ifndef ALLOC_H_
#define ALLOC_H_

#include <iostream>
#include <cstddef>
#include <cstdlib>

namespace MySTL
{

	/*
	* 第一级配置器
	* 当要求分配的内存足够大调用第一级配置
	* 或当第二级配置器已无可分配内存时调用第一级配置器,利用类似new-handler机制看是否有
	* 办法得到一些可分配内存,否则抛出bad_alloc异常
	*/
	// 以下是第一级配置器。
	//没有模板参数,所以其实尖括号里的inst没有用上
	template <int inst>
	class _malloc_alloc_template
	{
	private:
		//下面几个函数是用来处理内存不足的情况
		static void* oom_malloc(size_t);
		static void* oom_realloc(void *, size_t);
		static void(*_malloc_alloc_oom_handler)();

	public:
		static void* allocate(size_t n)
		{
			// 第一级配置器直接使用 malloc()
			void* result = malloc(n);
			//无法满足要求则改用oom_malloc
			if (0 == result) result = oom_malloc(n);
			return result;
		}

		static void deallocate(void *p, size_t /* n */)
		{
			// 第一级配置器直接使用 free()
			free(p);	             
		}

		static void* reallocate(void *p, size_t /* old_sz */, size_t new_sz)
		{
			//第一级配置器直接使用 realloc()
			//realloc函数用于修改一个原先已经分配的内存块的大小,可以使一块内存的扩大或缩小。
			void * result = realloc(p, new_sz);	
			//无法满足时调用oom_realloc
			if (0 == result) result = oom_realloc(p, new_sz);
			return result;
		}

		// 以下类似 C++ 的 set_new_handler().
		//函数set_malloc_handler,它接受一个void (*)()类型的参数f,返回类型为void (*)()。
		static void(*set_malloc_handler(void(*f)()))()
		{
			void(*old)() = _malloc_alloc_oom_handler;
			_malloc_alloc_oom_handler = f;
			return(old);
		}

	};

	// malloc_alloc out-of-memory handling
	template <int inst>
	void(*_malloc_alloc_template<inst>::_malloc_alloc_oom_handler)() = 0;

	//上面类里面的函数实现
	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;
			using std::bad_alloc;
			if (0 == my_malloc_handler)
			{
				std::cerr << "out of memory!" << std::endl;
				exit(1);
			}
			(*my_malloc_handler)();		// 呼叫处理程序,企图释放内存。
			result = malloc(n);			// 再次尝试配置内存。
			if (result) return(result);
		}
	}
	//上面类里面的函数实现
	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)
			{
				std::cerr << "out of memory!" << std::endl;
				exit(1);
			}
			(*my_malloc_handler)();	// 呼叫处理程序,企图释放内存。
			result = realloc(p, n);	// 再次尝试配置内存。
			if (result) return(result);
		}
	}

	typedef _malloc_alloc_template<0> malloc_alloc;  
	//第一级配置器到以上结束
	/*第一级配置器以malloc(),free(),realloc(),等C函数执行实际内存
	*配置、释放、重配置等操作,并且利用类似C++ new handler机制不能直接
	*使用,是因为其内存不是用::operator new来配置内存的。
	*如果内存不足会抛出异常或者利用exit(1)中止程序
	*/
	 //以下为配置器接口类
	template<class _T, class _Alloc>
	class simple_alloc
	{

	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));
		}
	};

	// 以下是第二级配置器。
	//源码中有线程相关的一个参数,这里省略。
	template <int inst>
	class _default_alloc_template
	{

	private:
		static const int _ALIGN = 8;            // 小型区块的上调边界
		static const int _MAX_BYTES = 128;      // 小型区块的上限
		static const int _NFREELISTS = 16;      //free-lists 个数(_MAX_BYTES / _ALIGN)
		
		//将bytes上调为8的倍数
		static size_t ROUND_UP(size_t bytes)    
		{
			//这段代码是常用的内存调整的常用工具,将输入bytes调整到ALLGN的倍数
			
			return (((bytes)+_ALIGN - 1) & ~(_ALIGN - 1));
		}
		//free list节点
		union obj                           
		{
			union obj* free_list_link;
			char client_data[1];    /* The client sees this. */
		};

	private:
		//数组指针,里面放着是指针指向一个obj。
		static obj* free_list[_NFREELISTS]; //16个自由链表
		//根据bytes大小决定使用的链表号
		static  size_t FREELIST_INDEX(size_t bytes)  
		{
			return (((bytes)+_ALIGN - 1) / _ALIGN - 1);
		}

		//allocate()时自由链表无可用空间,调用refill(),
		//返回一个大小为n的对象,并可能加入大小为n的其他区块到自由链表
		static void* refill(size_t n);
		//从内存池取空间,nobjs为传引用参数,分配一个大的空间,可以容纳
		//nobjs个size,如果配置这么多个不可以,这个值可能会较少
		static char* chunk_alloc(size_t size, int& nobjs); 

	    //内存池水位标志
		static char* start_free; //起始位置,只在chunk_alloc中变化
		static char* end_free;   //终止位置,只在chunk_alloc中变化
		static size_t heap_size; //大小

	public:
		static void * allocate(size_t n) /* n must be > 0 */
		{
			obj** my_free_list;
			obj* result;
			//大于128,调用第一级配置器
			if (n > (size_t)_MAX_BYTES)          
			{
				return(malloc_alloc::allocate(n));
			}
			//决定使用16个自由链表中的哪一个
			my_free_list = free_list + FREELIST_INDEX(n);  
			result = *my_free_list;
			if (result == 0)
			{
				//分配失败调用refill()
				void *r = refill(ROUND_UP(n));      
				return r;
			}
			//调整自由链表
			*my_free_list = result->free_list_link;
			return (result);
		};

		static void deallocate(void* p, size_t n) /* p may not be 0 */
		{
			obj* q = (obj*)p;
			obj** my_free_list;

			if (n > (size_t)_MAX_BYTES)     //调用第一级配置器
			{
				malloc_alloc::deallocate(p, n);
				return;
			}

			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);
	};

	typedef _default_alloc_template<0> alloc;
	typedef _default_alloc_template<0> single_client_alloc;
	//假定size已上调至8的倍数
	template <int inst>
	char* _default_alloc_template<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 = int(bytes_left / size);
			total_bytes = size * nobjs;
			result = start_free;
			start_free += total_bytes;
			return(result);
		}
		else  
		{
			//内存池余量不足一个区块
			//请求堆空间大小为2倍需求量加上一个随分配次数递增量
			size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);
			// 尝试利用内存池的零头
			if (bytes_left > 0)
			{
				//找寻适合的自由链表
				obj** my_free_list = free_list + FREELIST_INDEX(bytes_left);
				//调整自由链表,纳入这些零头
				((obj *)start_free)->free_list_link = *my_free_list;
				*my_free_list = (obj *)start_free;
			}
			//分配堆空间,补充内存池
			start_free = (char *)malloc(bytes_to_get);
			if (0 == start_free) //堆空间不足
			{
				int i;
				obj** my_free_list, *p;
				//找寻自由链表中含有尚未使用且足够大者
				for (i = (int)size; i <= _MAX_BYTES; i += _ALIGN)
				{
					my_free_list = free_list + FREELIST_INDEX(i);
					p = *my_free_list;
					if (0 != p)
					{
						*my_free_list = p->free_list_link;
						start_free = (char *)p;
						end_free = start_free + i;
						//递归调用自己,修正参数nobjs
						return(chunk_alloc(size, nobjs));
						//最终内存池残余的零头都被纳入适当的自由链表中
					}
				}
				end_free = 0; //山穷水尽,到处都没有内存可用
			//这时调用第一级配置器,利用其异常处理看是否有解决办法
				start_free = (char*)malloc_alloc::allocate(bytes_to_get);
			}
			heap_size += bytes_to_get;
			end_free = start_free + bytes_to_get;
			return(chunk_alloc(size, nobjs));
		}
	}


	//如果allocate()发现链表中没有可用区块,那么会调用这个函数
	//新的空间取自内存池(由chunk_alloc()完成),默认取得20个新节点(区块)
	//内存池不足可能会小于20个
	template <int inst>
	void* _default_alloc_template<inst>::refill(size_t n) //假定n已经上调为8的倍数
	{
		int nobjs = 20;
		//尝试从内存池分配所需空间取得nobjs个区块,nobjs是引用
		char* chunk = chunk_alloc(n, nobjs); 
		obj** my_free_list;
		obj* result;
		obj* current_obj, *next_obj;
		int i;
		//如果恰好只分配到一个区块,直接返回给调用者,自由链表没有新节点
		if (1 == nobjs) return(chunk);
		//否则调整自由链表以接纳剩余空间
		my_free_list = free_list + FREELIST_INDEX(n);

		//以下在内存池建立自由链表
		result = (obj *)chunk;//这一块返回给客端
	    //引导自由链表接纳从内存池分配的多余空间
		*my_free_list = next_obj = (obj *)(chunk + n);
		for (i = 1; ; i++) //编号0的区块返回给调用者
		{
			current_obj = next_obj;
			next_obj = (obj *)((char *)next_obj + n);
			if (nobjs - 1 == i)
			{
				current_obj->free_list_link = 0;
				break;
			}
			else
			{
				current_obj->free_list_link = next_obj;
			}
		}
		return(result);
	}

	template <int inst>
	void* _default_alloc_template<inst>::reallocate(void *p, size_t old_sz, size_t new_sz)
	{
		void * result;
		size_t copy_sz;

		if (old_sz > (size_t)_MAX_BYTES && new_sz > (size_t)_MAX_BYTES)
		{
			return(realloc(p, new_sz));
		}
		if (ROUND_UP(old_sz) == ROUND_UP(new_sz)) return(p);
		result = allocate(new_sz);
		copy_sz = new_sz > old_sz ? old_sz : new_sz;
		memcpy(result, p, copy_sz);
		deallocate(p, old_sz);
		return(result);
	}

	template <int inst>
	char* _default_alloc_template< inst>::start_free = 0;

	template <int inst>
	char* _default_alloc_template<inst>::end_free = 0;

	template <int inst>
	size_t _default_alloc_template<inst>::heap_size = 0;

	template <int inst>
	typename _default_alloc_template<inst>::obj*
		_default_alloc_template<inst> ::free_list[_NFREELISTS] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, };
}    //end of MySTL

#endif //end of AALOC_H_

        整个空间配置器的调用顺序如下:空间配置器的接口类为simple_alloc,这里可以选择是一级配置器或二级配置器,但是一般选择为二级配置器,因为二级配置器更为高效,二级配置器的整体调用顺序如下:首先调用allocate(),它从自由链表里拿需要的内存大小,如果取到内存,那么调整自由链表的节点,如果没有取到内存,那么调用refill()会从内存池取出默认20个区块放入自由链表,当然也有可能不够20个,如果不够20个就将有的区块返回,然后将第一个区块给客端,剩余的放入自由链表。refill函数还包含着第一次建立链表的功能。

        在refill函数里,从内存池去空间就会会调用chunk_alloc()函数,这个函数的功能:首先判断是否能够提供refill函数需要的空间,如果能满足则将这20个区块返回放入自由链表,如果不能满足,但是可以满足一个以上的区块,修改默认的20的值,改为现在的大小,然后将这些区块返回,如果连一个区块也不能满足,先根据需要的大小计算一个两倍的需求量加上一个随机值,得到这个需要申请的大小的量。然后先看内存池里有没有剩余的零头,将这些零头纳入到自由链表中,然后malloc()刚才的需求值,如果malloc没有申请到内存(堆空间不足)。chunk_alloc()函数会四处找看有没有可用的区块且区块够大,就从这里拿出一块,并调整自由链表指针,将内存池的剩下的内存放入自由链表,如果还是没有内存,就调用一级分配器。一级分配器还是调用的malloc,但是一级分配器有out-of-memory处理机制,或者有机会释放其他内存来使用。如果可以就成功,否则发出bad_alloc异常(上面代码替换成输出一个异常)。至此二级分配器的设计完成。

 

posted @ 2019-05-29 21:42  吾之求索  阅读(334)  评论(0)    收藏  举报