STL源码剖析-Allocator

一、第一级配置器

顾名思义,Allocator是用来做内存分配的。

第一级配置器指的是当需要128bytes以上的内存时直接使用malloc和realloc进行内存分配,代码如下:

	/*
	** @ 第一级配置器
	** @ 2023/04/07
	*/
	template<int __inst>
	class __malloc_alloc_template {
	private:
		static void* _S_oom_malloc(size_t);
		static void* _S_oom_realloc(void*, size_t);
		static void(*__malloc_alloc_oom_handler)();
	public:
		static void* allocate(size_t __n) {
			void* __result = malloc(__n);
			if (0 == __result)
				__result = _S_oom_malloc(__n);
			return __result;
		}

		static void deallocate(void* __p) {
			free(__p);
		}

		static void* reallocate(void* __p, size_t __new_sz) {
			void* __result = realloc(__p, __new_sz);
			if (0 == __result)
				__result = _S_oom_realloc(__p, __new_sz);
			return __result;
		}

		// 注册并返回旧的异常处理函数
		static void(*__set_malloc_handler(void(*__f)()))() {
			void(*__old)() = __malloc_alloc_oom_handler;
			__malloc_alloc_oom_handler = __f;
			return (__old);
		}
	};

	// 初始化静态成员
	template <int __inst>
	void(*__malloc_alloc_template<__inst>::__malloc_alloc_oom_handler)() = nullptr;

	template <int __inst>
	void*
		__malloc_alloc_template<__inst>::_S_oom_malloc(size_t __n) {
		void* __result;
		for (;;)
		{
			if (__malloc_alloc_oom_handler != nullptr)
				__malloc_alloc_oom_handler();
			__result = malloc(__n);
			if (__result)
				return __result;
		}
	}

	template <int __inst>
	void*
		__malloc_alloc_template<__inst>::_S_oom_realloc(void* __p, size_t __n) {
		void* __result;
		for (;;)
		{
			if (__malloc_alloc_oom_handler != nullptr)
				__malloc_alloc_oom_handler();
			__result = realloc(__p, __n);
			if (__result)
				return __result;
		}
	}

	typedef __malloc_alloc_template<0> malloc_alloc;

1、这里模板的定义为template<int __inst>是什么意思呢?既然指定了int类型为什么还要用模板呢?

__malloc_alloc_template中给出的是静态函数接口,所以callback/handler也得是静态的形式,但是我想让不同的Allocator拥有不同的callback/handler,那就只有定义多个Allocator类了(Allocator1,Allocator2,Allocator3......)。有没有办法可以解决这个问题?

template<int __inst>就可以解决上面的问题。我们用不同的参数显式实例化__first_stage_alloc,拿到的静态成员地址是不同的,所以我们就可以给他们注册不同的callback/handler函数。

2、__set_malloc_handler这是返回值为函数指针的函数~可以学习一下。

3、malloc(0) 会出现什么情况?是可以返回一个有效指针的,内存的大小是由机器决定的,具体可以去查阅更多资料。

二、第二级配置器

原先我以为这里的内存池是类似ringbuffer一样的东西,所以看这段代码时觉得很迷。其实维护的是这样一个东西:

bufferpool分为两个部分,一个部分是用_S_free_list维护的链表,另一部分是还未分配到链表的buffer。

buffer(小于等于128bytes)分配的步骤如下:

a.先从链表中获取buffer chunk

b.如果链表中没有了,从pool中获取chunk加入到链表中

c.如果pool中没有了,那么重新分配buffer到链表中,再返回给调用者

buffer(小于等于128bytes)销毁的步骤:找到对应链表,插入到链表头部

	template <int __inst>
	class __default_alloc_template {
	private:
		enum { _ALIGN = 8 };
		enum { _MAX_BYTES = 128 };
		enum { _NFREELISTS = _MAX_BYTES/_ALIGN };
		// 8 bytes align
		static size_t _S_round_up(size_t __bytes) {
			return (((__bytes)+(size_t)_ALIGN - 1) & ~((size_t)_ALIGN - 1));
		}

		union _Obj {
			union _Obj* _M_free_list_link;
			char _M_client_data[1];
		};

		// free list数组
		static _Obj* volatile _S_free_list[_NFREELISTS];

		// 根据区块大小决定使用第几号区块(0-15)
		static  size_t _S_freelist_index(size_t __bytes) {
			return (((__bytes)+(size_t)_ALIGN - 1) / (size_t)_ALIGN - 1);
		}
		// 
		static void* _S_refill(size_t __n);
		// 
		static char* _S_chunk_alloc(size_t __size, int& __nobjs);
		// 内存池起始位置
		static char* _S_start_free;	
		// 内存池结束位置
		static char* _S_end_free;		
		static size_t _S_heap_size;

	public:
		static void* allocate(size_t __n) {
			void* __ret = 0;
			if (__n > (size_t)_MAX_BYTES) {
				__ret = malloc_alloc::allocate(__n);
			}
			else
			{
				// 找到合适大小的链表
				_Obj* volatile* __my_free_list = _S_free_list + _S_freelist_index(__n);
				_Obj* __result = *__my_free_list;
				if (__result == 0)
				{
					__ret = _S_refill(_S_round_up(__n));
				}
				else {
					*__my_free_list = __result->_M_free_list_link;
					__ret = __result;
				}
			}
			return __ret;
		}

		static void deallocate(void* __p, size_t __n) {
			if (__n > (size_t)_MAX_BYTES)
				malloc_alloc::deallocate(__p, __n);
			else
			{
				_Obj* volatile* __my_free_list = _S_free_list + _S_freelist_index(__n);
				_Obj* __q = (_Obj*)__p;
				__q->_M_free_list_link = *__my_free_list;
				*__my_free_list = __q;
			}
		}

		static void* reallocate(void* __p, size_t __old_sz, size_t __new_sz);
	};

	template <int __inst>
	char* __default_alloc_template<__inst>::_S_start_free = 0;
	template <int __inst>
	char* __default_alloc_template<__inst>::_S_end_free = 0;
	template <int __inst>
	size_t __default_alloc_template<__inst>::_S_heap_size = 0;
	// 使用模板类对象,需要在类名前面使用typename
	template <int __inst>
	typename __default_alloc_template<__inst>::_Obj* volatile
		__default_alloc_template<__inst>::_S_free_list[
			__default_alloc_template<__inst>::_NFREELISTS
		] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, };

	template<int __inst>
	char* __default_alloc_template<__inst>::_S_chunk_alloc(size_t __size, int& __nobjs)
	{
		char* __result;
		size_t __total_bytes = __size * __nobjs;
		size_t __bytes_left = _S_end_free - _S_start_free;
		if (__bytes_left >= __total_bytes) {	/* 如果剩余空间总空间 > n x size,将剩余buffer加入到链表中,最多加20块*/
			__result = _S_start_free;
			_S_start_free += __total_bytes;
			return(__result);
		}
		else if (__bytes_left >= __size) {		/* n x size > 剩余空间总空间 > size, 将剩余buffer加入到链表中 */
			__nobjs = (int)(__bytes_left / __size);
			__total_bytes = __size * __nobjs;
			__result = _S_start_free;
			_S_start_free += __total_bytes;
			return(__result);
		}
		else { // 剩余空间总空间 < size
			// 分配2倍的total bytes buffer给buffer pool
			size_t __bytes_to_get =	2 * __total_bytes + _S_round_up(_S_heap_size >> 4);
			// 如果pool中还有剩余buffer,将剩余buffer加入到对应的链表中
			if (__bytes_left > 0) {
				_Obj* volatile* __my_free_list = _S_free_list + _S_freelist_index(__bytes_left);

				((_Obj*)_S_start_free)->_M_free_list_link = *__my_free_list;
				*__my_free_list = (_Obj*)_S_start_free;
			}
			// 分配buffer
			_S_start_free = (char*)malloc(__bytes_to_get);	
			if (0 == _S_start_free) {
				// 如果分配失败,则从其他链表中先获取一块buffer
				size_t __i;
				_Obj* volatile* __my_free_list;
				_Obj* __p;
				for (__i = __size; __i <= (size_t)_MAX_BYTES; __i += (size_t)_ALIGN) {
					__my_free_list = _S_free_list + _S_freelist_index(__i);
					__p = *__my_free_list;
					if (0 != __p) {
						*__my_free_list = __p->_M_free_list_link;
						_S_start_free = (char*)__p;
						_S_end_free = _S_start_free + __i;
						return(_S_chunk_alloc(__size, __nobjs));
					}
				}
				// 如果其他链表中也没有则调用第一级分配器中的allocate,这里有异常处理
				_S_end_free = 0;
				_S_start_free = (char*)malloc_alloc::allocate(__bytes_to_get);
			}
			_S_heap_size += __bytes_to_get;
			_S_end_free = _S_start_free + __bytes_to_get;
			// 将pool中的buffer加入到链表中
			return(_S_chunk_alloc(__size, __nobjs));
		}
	}

	template<int __inst>
	void* __default_alloc_template<__inst>::_S_refill(size_t __n)
	{
		// 每个链表最多维护20块buffer
		int __nobjs = 20;
		char* __chunk = _S_chunk_alloc(__n, __nobjs);
		_Obj* volatile* __my_free_list;
		_Obj* __result;
		_Obj* __current_obj;
		_Obj* __next_obj;
		int __i;

		if (1 == __nobjs) 
			return(__chunk);
		__my_free_list = _S_free_list + _S_freelist_index(__n);

		__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) {
				__current_obj->_M_free_list_link = 0;
				break;
			}
			else {
				__current_obj->_M_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 (_S_round_up(__old_sz) == _S_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);
	}

    typedef __default_alloc_template<0> alloc;

1、这里的8 bytes align很是巧妙:将原先的数字加7进位,然后用与运算减去余数。

2、使用union维护链表来节省内存

书里和网上查到的资料对此特性的描述:"用联合体union来维护链表,从其第一个字段观之可以视为一个指针,指向下一个区块;从其第二个字段观之是存有本块内存首地址"。一开始我不能理解这句话,后来我发现联合体中成员的地址等于联合体的地址:

	union _Obj {
		union _Obj* _M_free_list_link;
		char _M_client_data[1];
	};

	int v = 10;
	_Obj *o = (_Obj*)&v;
	printf("%x\n", o->_M_client_data);			// 0xb8fb2c
	printf("%x\n", &(o->_M_free_list_link));	// 0xb8fb2c

数组本身就是一个地址,打印数组就是打印的内存块的首地址,而这个数组具体有几个元素并不重要。

指针本身也是一个变量,有自己地址,成员指针的地址等同于union的地址,给成员指针赋值等同于给union存下一块buffer的地址。

3、用typename来表示这是一个类型

	template <int __inst>
	typename __default_alloc_template<__inst>::_Obj* volatile
		__default_alloc_template<__inst>::_S_free_list[
			__default_alloc_template<__inst>::_NFREELISTS
		] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, };

模板显式实例化: 

template typename TinySTL::__default_alloc_template<0>;

4、看到这里我还有几个问题?

deallocate并没有真正释放小于128bytes的buffer,随着buffer分配的越来越多,这里的处理是不是欠妥?

分配出来的buffer是一整块buffer中的一部分,写的时候不对size进行限制,指针越界怎么处理呢?

后续看到了再来解答.

三、标准空间配置器

上面是SGI的高效率配置器,除了这个,SGI还有一个标准的配置器,以下是精简的代码:

namespace TinySTL {
	typedef int	ptrdiff_t;

	template<typename T>
	inline T* allocate(ptrdiff_t size, T*) {
		T* tmp = (T*)(::operator new((size_t)(size * sizeof(T))));
		if (tmp == 0)
		{
			printf("out of memory\n");
			exit(1);
		}
		return tmp;
	}

	template<typename T>
	inline void deallocate(T* buffer) {
		::operator delete(buffer);
	}

	template<typename T>
	class allocator {
	public:
		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;
		
		pointer allocate(size_type n) {
			return TinySTL::allocate((difference_type)n, (pointer)0);
		}

		void deallocate(pointer p) {
			TinySTL::deallocate(p);
		}
	};
}

可以看到,allocate和deallocate分别调用的是::operator new和::operator delete,这是我第一次见到这种用法,网上查找资料可以知道,他们实际上是两个运算符,是调用的malloc和free在堆上申请、释放空间。new一个对象分为三个步骤,调用::operator new分配空间,构造对象和返回指针,一般来说我们不会去重载new运算符,用的是全局的。其他的相关内容可以参考new详解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

青山渺渺

感谢支持

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

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

打赏作者

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

抵扣说明:

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

余额充值