内存管理(四)SGI STL 空间配置器

前言

根据之前的学习,C++的内存申请的基本操作就是: 调用::operator new();释放内存的操作就是:调用 ::operator delete()。而二者是通过malloc() 和 free() 实现的。因此SGI空间配置器就是使用malloc() 和 free() 来代替C++的内存操作实现的内存管理。

正文

1.空间配置器的要求

SGI 对于空间配置器的设计提出了一下几点:
1.向 system heap 要求空间
2.考虑多线程状态
3.考虑内存不足时的应变措施
4.考虑过多“小型区块”可能造成的内存碎片问题
以下给出的代码和问题的探讨皆排除多线程状态。

2.SGI空间配置器的双层设计

SGI空间配置器设计了两层配置器,第一级配置器直接使用malloc() 和 free()来配置内存。第二级配置器分情况采取不同的策略:当需要申请的内存大于128字节时,视之为足够大,直接调用第一级配置器的接口;当需要申请的内存小于128字节时,为了避免多余的空间浪费(主要是cookie的空间),采用复杂的memory pool来管理内存。
一二级空间配置器的调用关系:
内存管理(四)SGI STL 空间配置器_第1张图片

3.一级空间配置器

typedef void(*H)();
template<int inst> //一级空间配置器
	class __malloc_alloc_template
	{
	private :
		
		//用来处理内存不足时的情况
		static void* oom_malloc(size_t); //oom:out of memory
		static void* 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 = oom_malloc(n);
			return result;
		}
		static void deallocate(void* p,size_t)
		{
			if (p)
				free(p);
		}
		static void* reallocate(void* p, size_t, size_t new_sz)
		{
			void* result = realloc(p, new_sz);
			if (0 == result)
				result = oom_realloc(p, new_sz);
			return result;
		}

		// 仿真C++的set_new_handler
		static H set_malloc_handler(H f)
		{
			H old = __malloc_alloc_oom_handler;
			__malloc_alloc_oom_handler = f;
			return old;
		}
	};
//template
//void (*__malloc_alloc_template::__malloc_alloc_oom_handler)() = 0;

template<int inst>
H __malloc_alloc_template<inst>::__malloc_alloc_oom_handler = 0;

template<int inst>
void*__malloc_alloc_template<inst>::oom_malloc(size_t n)
{
	H my_malloc_handler;
	void* result;
	while (1)  //不断尝试释放,配置
	{
		my_malloc_handler = __malloc_alloc_oom_handler;
		if (0 == __malloc_alloc_oom_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)
{
	H my_malloc_handler;
	void* result;
	while (1)
	{
		my_malloc_handler = __malloc_alloc_oom_handler;
		if (0 == __malloc_alloc_oom_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() 进行了一层封装并且通过模拟C++中的 new_handler 机制来接解决内存不足的问题。
以allocate函数为例,会先调用malloc来直接配置内存,如果配置失败调用oom_malloc,在oom_malloc中,如果有设置__malloc_alloc_oom_handler,则不断循环调用该回调并配置内存直到成功,否则直接抛出异常。
set_malloc_handler函数与C++ 中的_set_new_handler函数功能一致。
总体来说,第一级空间配置器就是对C++中的内存配置和释放机制的浅封装。

4.二级空间配置器

#define __ALIGN 8
#define __MAX_BYTES 128
#define __NFREELISTS  __MAX_BYTES/__ALIGN
#define __OBJNUM 20

template<int inst>
class __default_alloc_template   //二级空间配置器
{
private:
	static size_t ROUND_UP(size_t bytes)  //将bytes调至8的倍数
	{
		return ((bytes)+__ALIGN - 1) & ~(__ALIGN - 1);
	}
    struct obj  //嵌入式指针
	{
		struct obj* free_list_link;
	};

public:
    static obj* free_list[__NFREELISTS];  //根据大小区分的空闲内存区块
	static size_t FREELIST_INDEX(size_t bytes) //根据字节确定区块索引
	{
		return ((bytes)+__ALIGN - 1)/(__ALIGN - 1);
	}

	static void* refill(size_t n);

	static char* chunk_alloc(size_t size, int &nodjs);

	static char* start_free; //内存池初始位置
	static char* end_free;   //结束位置
	static size_t heap_size;

 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);
};
typedef __default_alloc_template<0> alloc;
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 };

二级空间配置器维护着16个链表,由一个静态的指针数组来存储。其中每一个元素都是一个链表的头。每一个链表中挂着相应大小的内存块若干(数量不定,<=20,可以为空),并且每一次申请的空间必须满足8字节的倍数,不足的由 ROUND_UP函数调整到8字节的倍数。

(一)allocate函数

template<int inst>
void* __default_alloc_template<inst>::allocate(size_t n)  //申请内存
{
	obj** my_free_list;
	obj* result;

	if (n > (size_t)__MAX_BYTES)   //大于128调用一级空间配置器
	{
		return malloc_alloc::allocate(n);
	}
	my_free_list = free_list + FREELIST_INDEX(n);

	result = *my_free_list;
	if (0 == result)   //没有可用的free list,使用refill函数填充
	{
		void* r = refill(ROUND_UP(n));
		return r;
	}

	*my_free_list = result->free_list_link;   //result 和 *my_free_list 此时指向同一块内存
	return (result);

}

二级空间配置器的allocate有两种决策:当申请内存大于128字节时,直接调用一级配置器的allocate;当小于128字节的时候,找到相应大小字节的链表头(由FREELIST_INDEX算出目标元素与指针数组首地址的偏移量)。如果此时链表中由元素,直接返回第一块,并更新链表头。否则调用refill函数填充链表后返回第一块。

(二)deallocate函数

template<int inst>
void __default_alloc_template<inst>::deallocate(void* p, size_t n)
{
	obj* q = (obj*)p;
	obj** my_free_list;
	if(n > (size_t)__MAX_BYTES)   //大于128调用一级空间配置器
	{
		 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;
}

同样的,对于deallcoate函数,先判断释放内存的大小,如果大于128字就调用一级配置器的接口,否则将该内存块归还到相应的链表中,更新链表头。

(三)refill函数

template<int inst>
void* __default_alloc_template<inst>::refill(size_t n)
{
	int nobjs = __OBJNUM;
	char* chunk = chunk_alloc(n, nobjs);
	obj** my_free_list;
	obj* result;
	obj* current_obj;
	obj* next_obj;
	if (1 == nobjs)   //只获得1块,直接返回,不向free_list 中新加节点
		return chunk;
	my_free_list = free_list + FREELIST_INDEX(n);
	result = (obj*)chunk;
	//让my_free_list指向第二个节点(因为第一个节点要被返回,不用添加进free_list)
	*my_free_list = next_obj = (obj*)(chunk + n);
	for (int i = 1;i<nobjs-1; i++)
	{
		current_obj = next_obj;
		next_obj = (obj*)((char*)next_obj + n);
		current_obj->free_list_link = next_obj;
	}
	next_obj->free_list_link = 0;
	return result;
}

先调用chunk_alloc函数从内存池中获取一定数量内存块(最大不超过__OBJNUM块),获取到的块数由nobjs返回,如果只获取到了一块,直接将内存返回给上一层,不用将之添加到链表中;若是多余一块,将剩余的内存块添加到链表中。

(四)chunk_alloc函数

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)  //剩余空间不够满足全部需求,但是可以满足一个或以上的区块(不到20个)
	{
		nobjs = bytes_left / size;
		total_bytes = nobjs * size;
		result = start_free;
		start_free += total_bytes;
		return result;
	}
	else  //剩余空间一块都满足不了
	{
		//存在小的内存碎片,将之挂到相应大小的free_list上
		//(因为每次都是以8的倍数拿取,所以可以在free_list中找到同样大小的块)
		if (bytes_left > 0)  
		{
			obj** my_free_list;
			my_free_list = free_list + FREELIST_INDEX(bytes_left);
			((obj*)start_free)->free_list_link = *my_free_list;
			*my_free_list = (obj*)start_free;
		}

		//申请堆内存来填充内存池
		size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);
		start_free = (char*)malloc(bytes_to_get);
		if (0 == start_free) //分配失败
		{
			obj** my_free_list;
			obj* p;
			for (int i = size; i <= __MAX_BYTES; i += __ALIGN)  //寻找比size大的区块
			{
				my_free_list = free_list + FREELIST_INDEX(i);
				p = *my_free_list;
				if (p)      //如果由空闲的free_list取出一块给内存池
				{
					*my_free_list = p->free_list_link;
					start_free = (char*)p;
					end_free = start_free + i;
					return chunk_alloc(size, nobjs);  //递归调用,修正nobjs的值
				}
			}
			//没有拿到合适的区块,转而调用一级空间配置器
			//尝试使用out of memory 机制释放一部分内存
			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);
	}
}

chunk_alloc函数是整个配置器中的难点和精髓部分。首先检测内存池中剩余空间是否满足全部需求(__OBJNUM块的内存),如果满足直接把内存返回给上一层,并更新内存池的剩余量。如果不能满足全部需求(大于等于1,小于__OBJNUM块),但可以满足一块或者以上,就返回可以返回的最大块数的内存。但是如果连一块内存都满足不了的情况,回先将内存池中剩余小的区块(此时可以视之为内存碎片)挂载到相应的链表上。然后用malloc申请堆内存来填充内存池,此时的申请的内存总量等于需求量的二倍,加上追加量,并调到8字节的倍数。这时候如果malloc失败的话,就在链表中找到一个比需求区块大并且相差最小的链表,从中拿出一块放回内存池后递归调用chunk_alloc来修正nobjs。但是若是出现最差的情况,所有的链表表中都没有符合的空闲内存可以用时,就会调用一级配置器中的allocate来尝试oom机制是否可以解决这个问题,就会出现上述一级配置器中成功返回和抛出异常的结果。(如果__malloc_alloc_oom_handler设计的不好可能出现死循环,也就是不断地待用回调,但还是分配不到空间)

4.一二级配置器统一封装的接口

想要在STL标准的容器中使用需要满足标准的接口,下面的封装实现了配置器接口的转调用。

template<class T,class Alloc>
	class simple_alloc
	{
	public:
		static T* allocate(size_t n)  //申请多个时调用接口
		{
			return 0 == n ? n : (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);
		}
	};

5.测试

#include
#include

namespace wlj
{
	typedef void(*H)();
	//封装的统一接口
	template<class T,class Alloc>
	class simple_alloc
	{
	public:
		static T* allocate(size_t n)  //申请多个时调用接口
		{
			return 0 == n ? n : (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);
		}
	};

	//==================================================================================================

	template<int inst> //一级空间配置器
	class __malloc_alloc_template
	{
	private :
		
		//用来处理内存不足时的情况
		static void* oom_malloc(size_t); //oom:out of memory
		static void* 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 = oom_malloc(n);
			return result;
		}
		static void deallocate(void* p,size_t)
		{
			if (p)
				free(p);
		}
		static void* reallocate(void* p, size_t, size_t new_sz)
		{
			void* result = realloc(p, new_sz);
			if (0 == result)
				result = oom_realloc(p, new_sz);
			return result;
		}

		// 仿真C++的set_new_handler
		static H set_malloc_handler(H f)
		{
			H old = __malloc_alloc_oom_handler;
			__malloc_alloc_oom_handler = f;
			return old;
		}
	};
//template
//void (*__malloc_alloc_template::__malloc_alloc_oom_handler)() = 0;

template<int inst>
H __malloc_alloc_template<inst>::__malloc_alloc_oom_handler = 0;

template<int inst>
void*__malloc_alloc_template<inst>::oom_malloc(size_t n)
{
	H my_malloc_handler;
	void* result;
	while (1)  //不断尝试释放,配置
	{
		my_malloc_handler = __malloc_alloc_oom_handler;
		if (0 == __malloc_alloc_oom_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)
{
	H my_malloc_handler;
	void* result;
	while (1)
	{
		my_malloc_handler = __malloc_alloc_oom_handler;
		if (0 == __malloc_alloc_oom_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;
   //====================================================================================
#define __ALIGN 8
#define __MAX_BYTES 128
#define __NFREELISTS  __MAX_BYTES/__ALIGN
#define __OBJNUM 20

template<int inst>
class __default_alloc_template   //二级空间配置器
{
private:
	static size_t ROUND_UP(size_t bytes)  //将bytes调至8的倍数
	{
		return ((bytes)+__ALIGN - 1) & ~(__ALIGN - 1);
	}
    struct obj  //嵌入式指针
	{
		struct obj* free_list_link;
	};

public:static obj* free_list[__NFREELISTS];  //根据大小区分的空闲内存区块
	static size_t FREELIST_INDEX(size_t bytes) //根据字节确定区块索引
	{
		return ((bytes)+__ALIGN - 1)/(__ALIGN - 1);
	}

	static void* refill(size_t n);

	static char* chunk_alloc(size_t size, int &nodjs);

	static char* start_free; //内存池初始位置
	static char* end_free;   //结束位置
	static size_t heap_size;

 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);
};
typedef __default_alloc_template<0> alloc;
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 };


template<int inst>
void* __default_alloc_template<inst>::allocate(size_t n)  //申请内存
{
	obj** my_free_list;
	obj* result;

	if (n > (size_t)__MAX_BYTES)   //大于128调用一级空间配置器
	{
		return malloc_alloc::allocate(n);
	}
	my_free_list = free_list + FREELIST_INDEX(n);

	result = *my_free_list;
	if (0 == result)   //没有可用的free list,使用refill函数填充
	{
		void* r = refill(ROUND_UP(n));
		return r;
	}

	*my_free_list = result->free_list_link;   //result 和 *my_free_list 此时指向同一块内存
	return (result);

}


template<int inst>
void __default_alloc_template<inst>::deallocate(void* p, size_t n)
{
	obj* q = (obj*)p;
	obj** my_free_list;
	if(n > (size_t)__MAX_BYTES)   //大于128调用一级空间配置器
	{
		 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;
}


template<int inst>
void* __default_alloc_template<inst>::refill(size_t n)
{
	int nobjs = __OBJNUM;
	char* chunk = chunk_alloc(n, nobjs);
	obj** my_free_list;
	obj* result;
	obj* current_obj;
	obj* next_obj;
	if (1 == nobjs)   //只获得1块,直接返回,不向free_list 中新加节点
		return chunk;
	my_free_list = free_list + FREELIST_INDEX(n);
	result = (obj*)chunk;
	//让my_free_list指向第二个节点(因为第一个节点要被返回,不用添加进free_list)
	*my_free_list = next_obj = (obj*)(chunk + n);
	for (int i = 1;i<nobjs-1; i++)
	{
		current_obj = next_obj;
		next_obj = (obj*)((char*)next_obj + n);
		current_obj->free_list_link = next_obj;
	}
	next_obj->free_list_link = 0;
	return result;
}

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)  //剩余空间不够满足全部需求,但是可以满足一个或以上的区块(不到20个)
	{
		nobjs = bytes_left / size;
		total_bytes = nobjs * size;
		result = start_free;
		start_free += total_bytes;
		return result;
	}
	else  //剩余空间一块都满足不了
	{
		//存在小的内存碎片,将之挂到相应大小的free_list上
		//(因为每次都是以8的倍数拿取,所以可以在free_list中找到同样大小的块)
		if (bytes_left > 0)  
		{
			obj** my_free_list;
			my_free_list = free_list + FREELIST_INDEX(bytes_left);
			((obj*)start_free)->free_list_link = *my_free_list;
			*my_free_list = (obj*)start_free;
		}

		//申请堆内存来填充内存池
		size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);
		start_free = (char*)malloc(bytes_to_get);
		if (0 == start_free) //分配失败
		{
			obj** my_free_list;
			obj* p;
			for (int i = size; i <= __MAX_BYTES; i += __ALIGN)  //寻找比size大的区块
			{
				my_free_list = free_list + FREELIST_INDEX(i);
				p = *my_free_list;
				if (p)      //如果由空闲的free_list取出一块给内存池
				{
					*my_free_list = p->free_list_link;
					start_free = (char*)p;
					end_free = start_free + i;
					return chunk_alloc(size, nobjs);  //递归调用,修正nobjs的值
				}
			}
			//没有拿到合适的区块,转而调用一级空间配置器
			//尝试使用out of memory 机制释放一部分内存
			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);
	}
}

}
template<class T,class Alloc = wlj::alloc>
class A
{
protected:
	typedef wlj::simple_alloc<T, Alloc> data_allocator;
public:
	T* get_instance(T n)
	{
		T* result = data_allocator::allocate();
		*result = n;
		return result;
	}
};

int main()
{
	
	A<int>a;
	int* val = a.get_instance(10);
	std::cout << *val << std::endl;

}

整体的测试一下。

6. 总结与思考

1.采用二层配置的机制,在不同内存需求的情况下采取不同的策略。造成内存浪费的主要原因是由于操作系统需要通过cookie来管理分配出去的内存,用malloc每次分配内存都会额外的有8字节的开销用于存放cookie,相较于大的区块,8字节所占的比例很小,内存的浪费可以接受,但是对于大量的小区块,cookie的开销就显得格外庞大。能过通过内存池的方式管理内存的客观条件就是我们在释放内存时不需要访问cookie就已经知道了此次需要释放的内存大小,也就无需cookie,所以对于所有的小区块进行了调整,让其满足于8字节的倍数,在释放时通过简单的计算就可以直接定位到相应的链表并归还。
2.在处理内存碎片问题上,直接将当前的碎片挂在到相应链表中去。
3.在chunk_alloc 无法 malloc 到内存时,只是找寻比需求内存大的链表,就会出现一种情况:大的链表都没有空闲的内存,但是比之小的链表其实还有很多内存,引出一个思考:小的区块是不是也可以用呢?
答案是很难实现,因为链表的区块与区块之间并不一定是真正连续的,即使多块总和满足需求,但是不连续是没用的。
4.在chunk_alloc 调用 malloc 时只以bytes_to_get为大小调用了一次,没有再次尝试改变bytes_to_get(将之变小)后调用malloc,其实这样的方式也是解决的一种办法,但是SGI空间配置器中并没有进行这样的设计,原因是:对于多进程的系统来说,如果某一个进程将所有能用的内存都占用,对于其他进程是灾难性的,避免竭泽而渔是一种很睿智的举动。

你可能感兴趣的:(c++,内存管理,sgi,stl)