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異常。

以上爲第二級空間配置器設計。
至此,除了考慮多線程時的處理,其他邏輯已剖析完畢。

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