stl_alloc.h源码剖析

本博客结合<<侯捷老师的STL源码剖析>>阅读更佳,
本文主要介绍stl_alloc.h 源码
  • 内存配置操作由 alloc:allocate()负责
  • 内存释放操作由alloc::deallocate()负责

stl_construct.h

  • 对象构造操作由::construct()负责
  • 对象析构操作由::destory()负责
空间的配置与释放 std::alloc

SGI设计哲学:

  • 向system heap要求空间
  • 考虑多线程情况
  • 考虑内存不足应变措施
  • 考虑过多"小型区块"可能造成的内存碎片(fragment)问题。

C++内存配置基本操作是::operator new();内存释放基本操作是::operator delete();这两个全局函数相当于c的malloc()和free函数。SGI正是以malloc()和free();完成内存的配置与释放。

考虑到小型区块所可能造成的内存破碎问题,SGI设计了双层级配置器,第一级配置器直接使用malloc()和free(),第二级配置器视情况采取了不同的策略:当配置区块超过128bytes,视之为"足够大",便调用第一级配置器:当配置区块小于128bytes时,视之为"过小",为了降低额外负担(overhead),便采用复杂的memory pool整理方式,而不再求助于第一级配置器。

stl_alloc.h源码分析
typedef __malloc_alloc_template<0> malloclloc;
typedef malloc_alloc alloc;	 第一级配置器
//default_alloc效率高
typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc; 第二级配置器
stl规格配置器接口
  • 单纯的转调用
  • SGI容器全都是用这个simple_alloc接口
举个例子,详细介绍在后面专门介绍具体容器时介绍
默认使用 alloc 配置器
template <class T, class Alloc = alloc> 
class list {
protected:
	typedef void* void_pointer;
	typedef __list_node<T> list_node;
	//空间配置器
	typedef simple_alloc<list_node, Alloc> list_node_allocator;
	//配置一个节点 allocate分配内存
	//在insert或者是push元素时会触发
	link_type get_node() { return list_node_allocator::allocate(); }
	//释放一个节点 deallocate释放内存
	//erase时会触发
	void put_node(link_type p) { list_node_allocator::deallocate(p); }
	....
}
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));
	}
};
第一级配置器 __malloc_alloc_template 剖析
define __THROW_BAD_ALLOC throw bad_alloc

// 第一级配置器。
// 模板参数没用到
template <int inst>
class __malloc_alloc_template {

private:
	//处理内存不足的情况
	static void *oom_malloc(size_t);
	static void *oom_realloc(void *, size_t);

#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
	static void(*__malloc_alloc_oom_handler)();
#endif

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)
	{
		void * result = realloc(p, new_sz);	// 第一级配置器直接使用 realloc()
		// 以上无法满足需求时调用oom_realloc
		if (0 == result) result = oom_realloc(p, new_sz);
		return result;
	}

	// 以下类似于 C++ 的 set_new_handler().
	// f为函数指针,异常处理
	// 设置out-of-memory hanlder。
	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

#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
template <int inst>
// 内存不足时异常处理的函数指针,初始为0
void(*__malloc_alloc_template<inst>::__malloc_alloc_oom_handler)() = 0;
#endif

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

template <int inst>
void * __malloc_alloc_template<inst>::oom_realloc(void *p, size_t n)
{
	void(*my_malloc_handler)();
	void *result;
	//跟oom_malloc 一样的处理逻辑
	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);
	}
}

SGI第一级配置器的 allocate()和realloc都是在调用malloc和realloc();不成功后,改调用oom_malloc()和oom_realloc();后两者都有内循环不断调用客户端注册的"__malloc_alloc_oom_handler “,以期望在某次调用之后获得足够的内存而圆满完成任务,但是如果没有注册”__malloc_alloc_oom_handler ",那么oom_malloc()和oom_realloc()便直接丢出bad_alloc异常信息或者exit(1)硬生生终止程序。

第二级配置器 __default_alloc_template剖析
  • 第二层配置器多了一些机制,避免太多小额区块造成内存的随便,小额区块带来的其实不仅是内存随便,配置时额外的负担(overhead)也是一个大问题(write operator delete if you write operator new),额外的负担永远无法避免,毕竟系统要靠这多出来的空间来管理内存,但是区块越小,额外负担所占的比例越大,就越浪费

    索求任何一块内存,都得有一些"税"要缴给系统。申请内存时需要额外的内存cookie来记录内存大小以便于在释放时需要知道如何处理。因此小区块申请越多越浪费。

  • SGI第二层配置器的做法是,如果区块够大,超过128byte时,就移交第一级配置器处理。当区块小于128bytes时,则以内存池(memory pool)管理,此法又称为次层配置(sub-allocation):每次配置一大块内存,并维护对应之自由链表(free-list),下次若再有相同大小的内存需求,就直接从free-list中拨出,如果客户端释放小额区块,就由配置器回收到free-list中,—负责配置,也负责回收。
  • 为了方便管理,SGI第二级配置器会主动将任何小额区块的内存需求量上调至8的倍数,并维护16个free-list,各自管理大小分别为8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128的小额区块。
union 节省内存
union obj {
		union obj * free_list_link; //指向下一个free-list的节点
		char client_data[1];    		//指向实际的内存区块
	};

__default_alloc_template源码剖析

class __default_alloc_template {

private:
	//最小区块
	enum { __ALIGN = 8 };
	//最大区块
	enum { __MAX_BYTES = 128 };
	//链表节点数(区块档位数)
	enum { __NFREELISTS = __MAX_BYTES / __ALIGN };
	//将bytes上调大小至8的倍数
	//bytes + 7 再按位与 11111000 
	static size_t ROUND_UP(size_t bytes) {
		return (((bytes)+__ALIGN - 1) & ~(__ALIGN - 1));
	}
private:
	//链表数组
	static obj * __VOLATILE free_list[__NFREELISTS];
	//获取链表数组下标 bytes + 7后 再对8整除 再减一 数组从0
	static  size_t FREELIST_INDEX(size_t bytes) {
		return (((bytes)+__ALIGN - 1) / __ALIGN - 1);
	}

	// Returns an object of size n, and optionally adds to size n free list.
	// 返回一个大小为n的对象,并可能加入大小为n的其他区块到free_list
	static void *refill(size_t n);
	// Allocates a chunk for nobjs of size "size".  nobjs may be reduced
	// if it is inconvenient to allocate the requested number.
	//配置一大块空间,可容纳nobjs个大小为size的区块
	//如果内存不足nobjs可能会降低
	static char *chunk_alloc(size_t size, int &nobjs);

	// Chunk allocation state.
	static char *start_free;//内存池起始位置 只在chunk_alloc中变化
	static char *end_free;//内存池结束位置,只在chunk_alloc中变化
	static size_t heap_size;//累计分配内存的大小

//线程,锁相关部分后面再分析
//todo 线程

public:

	/* n must be > 0      */
	static void * allocate(size_t n)
	{
		obj * __VOLATILE * my_free_list;
		obj * __RESTRICT 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) {
			//没找到可用的free_list,重新填充free_list。
			//看下面refill函数
			void *r = refill(ROUND_UP(n));
			//返回分配的内存
			return r;
		}
		//如果找到了合适的链表
		//调整free_list第一个链表节点的指向
		//将my_free_list也就是该类型链表的第一个节点指向第二个节点
		//因为这个链表上的第一块内存要被分配使用了
		*my_free_list = result->free_list_link;
		return (result);
	};

	/* p may not be 0 */
	static void deallocate(void *p, size_t n)
	{
		obj *q = (obj *)p;
		obj * __VOLATILE * my_free_list;
		//大于128时,用第一级配置器 直接调用free
		if (n > (size_t)__MAX_BYTES) {
			malloc_alloc::deallocate(p, n);
			return;
		}
		//寻找对应的free_list。
		my_free_list = free_list + FREELIST_INDEX(n);
		//将要回收的区块的下个节点指向第一个节点
		q->free_list_link = *my_free_list;
		//调整free_list对应链表的第一个节点,指向为要回收的那个区块
		*my_free_list = q;
	}
//当free_list没有可用区块时,调用refill(),准备为free_list重新填充空间,新的空间将取自内存池(由chunk_alloc()完成)。缺省获得20个新区块,万一内存不足时,获得的区块数可能小于20
	template <bool threads, int inst>
	void* __default_alloc_template<threads, inst>::refill(size_t n)
	{
		int nobjs = 20;
		//调用chunk_alloc();尝试获得nobjs个区块作为free_list的新节点
		//chunk_alloc()下个节点详述
		char * chunk = chunk_alloc(n, nobjs);
		obj * __VOLATILE * my_free_list;
		obj * result;
		obj * current_obj, *next_obj;
		int i;
		//当只有一个区块时,就分配给调用者使用,free_list没有新的节点
		if (1 == nobjs) return(chunk);
		//从链表数组中那对应类型的第一个链表节点。
		//准备调整free_list大小
		my_free_list = free_list + FREELIST_INDEX(n);

		//类型强转,这块内存返回给调用者使用
		//类型强转,就成了一块内存
		//类型决定了数据的存储格式和读取格式,存储格式描述了数据存储所占用的空间,读取格式描述了存储空间中的数据已何种方式组装,这两种格式决定了一种“类型“
		result = (obj *)chunk;
		//free_list指向新配置的空间(抛去了返回给调用者的那块内存)
		//将各个节点串起来
		*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) {
				//最后一块内存的next链表指针指向空
				current_obj->free_list_link = 0;
				break;
			}
			else {
				//当前块的next链表指针指向下个节点
				current_obj->free_list_link = next_obj;
			}
		}
		return(result);
	}
};

//从内存池中取空间给free_list使用
template <bool threads, int inst>
char*
__default_alloc_template<threads, inst>::chunk_alloc(size_t size, int& nobjs)
{
	char * result;
	//请求分配的大小,给free_list(第一块给调用者,其他的放到free_list)
	size_t total_bytes = size * nobjs;
	//内存池剩余大小
	size_t bytes_left = end_free - start_free;
	//内存池大小足够nobjs个size空间
	if (bytes_left >= total_bytes) {
		//返回
		result = start_free;
		//内存池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移动,此时内存池大小小于一个区块的size
		start_free += total_bytes;
		return(result);
	}
	//当内存池大小,小于一个区块的size时
	else {
		//尝试分配 2倍的 请求内存大小 外加附加量
		//heap_size是以往内存池的容量的累加和
		//head_size作为一个附加变量来看待,要满足,随着“加水”次数变多,每次加水的量应该越来越大这个条件
		size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);
		// Try to make use of the left-over piece.
		// 充分利用内存池中剩下的内存
		if (bytes_left > 0) {
			//看看链表数组里有没有合适的链表
			//虽然此时内存池中不够一个区块的大小
			//但是可能满足较小的区块
			obj * __VOLATILE * my_free_list =
				free_list + FREELIST_INDEX(bytes_left);
			//调整free_list将内存池中剩余的内存编入链表
			//内存池中的内存强转为obj,并指向头结点
			((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 * __VOLATILE * my_free_list, *p;
			// Try to make do with what we have.  That can't
			// hurt.  We do not try smaller requests, since that tends
			// to result in disaster on multi-process machines.
			//内存不足时,尝试链表上是否有尚未使用的区块,区块足够大
			//区块足够大的意思是起码大于一个区块的size。
			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中的区块释放到内存池
					*my_free_list = p->free_list_link;
					start_free = (char *)p;
					end_free = start_free + i;
					//此时内存池有一个满足的区块了
					//递归调用,调整nobjs大小。此时nobjs是1
					return(chunk_alloc(size, nobjs));
				}
			}
			end_free = 0;	
			// In case of exception. 
			//如果还是没有找到可用的区块,调用第一类分配器
			//第一类分配器有异常处理函数,有机会释放其他内存拿来此处使用,也会抛异常
			start_free = (char *)malloc_alloc::allocate(bytes_to_get);
		}
		//申请分配内存成功
		//heap_size以往内存池的容量的累加和
		heap_size += bytes_to_get;
		end_free = start_free + bytes_to_get;
		//此时内存池有了内存 2倍的 nobjs * size  + 付加量
		//递归调用,返回给调用者请求的内存,调整start_free的大小
		return(chunk_alloc(size, nobjs));
	}
}
第二级配置器的设计概述

上述chunk_alloc()函数以end_free - start_free来判断内存池的水量

  • 如果水量充足直接调出20个区块,1个区块交出给调用者,19个交给对应的free_list维护。
  • 如果水量不足以20个区块,但还足够1个及以上的区块。1个区块交出给调用者,剩余区块交给对应的free_list维护。
  • 如果连一个区块都不满足,则用malloc请求分配,向内存池中注入活水

    新水量的大小为需求量的两倍,再加上一个随着配置次数增加而越来越大的附加量(ROUND_UP(heap_size >> 4))。

  • 当system heap空间都不够用了时,malloc()失败,chunk_alloc()就四处寻找"尚未有未用的区块,且足够大"之free_list。找到了就从free_list挖出来放到内存池中。找不到就调用第一级配置器。第一级配置器也是调用malloc()来配置内存,但是它有oom(out-of-memory)处理机制,或许有机会释放其他内存拿来此处使用,如果可以就成功,否则抛出异常bad_alloc异常。

以上为第二级空间配置器设计。
至此,除了考虑多线程时的处理,其他逻辑已剖析完毕。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章