1. 程式人生 > >深度解析--SGI STL 空間配置與釋放

深度解析--SGI STL 空間配置與釋放

歡迎大家來訪二笙的小房子,一同學習分享生活!

文章目錄

1.SGI 空間配置與釋放

  • 物件構造前的空間配置與物件析構後的空間釋放,由<stl_alloc.h>負責,SGI主要考慮以下四個步驟:
    1.向system heap要求空間
    2.考慮多執行緒狀態
    3.考慮記憶體不足時的應對措施
    4.考慮過多“小型區塊”可能造成的記憶體碎片問題
  • SGI以malloc()和free()完成記憶體的配置與釋放,但考慮到小型區塊可能造成的記憶體破碎問題,SGI設定了雙層級配置器:
    1.第一級配置器直接使用malloc()和free()
    2.第二級配置器視情況採用不同的策略:
  • 配置區塊超過128bytes時,視之為足夠大,便採用第一級配置器
  • 配置區塊小於128bytes時,視之為過小,採用memory pool整理方式

2. 兩級配置器

  • SGI封裝了一個介面,名為simple_alloc使配置器的介面符合STL規格),無論alloc被定義為第一級亦或第二級配置器,都採用該介面進行空間配置:
template <class T, class Alloc>
class simple_alloc {
public:
	static T *allocate(size_ t n)      //單純的轉呼叫函式,呼叫傳遞給配置器的成員函式
{return 0 == n? 0 : (T*) Alloc::allocate(n * szieof(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)); } sattic void deallocate(T *p) { Alloc::deallocate(p, sizeof(T)); } };

2.1 一、二級配置器的關係

  • 在學習兩級配置器之前首先通過下圖瞭解一下一、二級配置器是如何實現的以及兩者之間的關係:
    第一級配置器與第二級配置器

2.2 第一級配置器 __malloc_alloc_template

  • 主要以malloc()、free()、realloc()等C函式執行實際的記憶體配置、釋放、重配置操作,並實現出C++ new-handler機制:
#include <iostream>
#define __throwBadAlloc  cerr << "out of memory" << endl;  exit(1);

template <int inst>
class __MallocAllocTemplate {
	typedef void(*OOM_HANDLE)();
private:
	//以下函式用來處理記憶體不足的情況,oom:out  of  memory
	static void  *oomMalloc(size_t);
	static void  *oomRealloc(void *, size_t);
	static OOM_HANDLE OOM_Handle;

public:
	static void * allocate(size_t n) {
		void *result = malloc(n);      //第一級配置器直接使用malloc
		//如果記憶體不足則呼叫oomMalloc處理
		if (0 == result)   result = oomMalloc(n);
		return result;
	}

	static void deallocate(void *p, size_t m) {
		free(n);    //第一級配置器直接呼叫free
	}

	static void * reallocate(void *p, size_t oldSz, size_t newSz) {
		void *result = realloc(p, newSz);   //第一級配置器直接呼叫realloc()
		//記憶體不足呼叫oomRealloc
		if (0 == result)  result = oomRealloc(p, newSz);
		return result;
	}

	//static void (* set_malloc_handle (void (*f)())) ()
	//引數與返回值都是void (*)()
	static OOM_HANDLE  setMallocHandele(OOM_HANDLE f) {
		OOM_HANDLE old = OOM_Handle;
		OOM_Handle = f;
		return old;
	}
};

//設定初始指標為空
template <int inst>
void(*__MallocAllocTemplate<inst>::OOM_Handle)() = NULL;

//記憶體不足嘗試再次申請
template <int inst >
void * __MallocAllocTemplate<inst>::oomMalloc(size_t n) {
	void(*myMallocHandle)();
	void *result;
	for (;;) {
		myMallocHandle = OOM_Handle;    //不斷嘗試釋放、配置、再釋放、再配置...
		if (0 == myMallocHandle) { __throwBadAlloc;  }
		(*myMallocHandle)();         //呼叫處理例程,企圖釋放記憶體
		result = malloc(n);          //再次嘗試配置記憶體
		if (result)  return(result);
	}
}

template <int inst >
void * __MallocAllocTemplate<inst>::oomRealloc(void *p, size_t n) {
	void(*myMallocHandle)();
	void *result;
	for (;;) {
		myMallocHandle = OOM_Handle;    //不斷嘗試釋放、配置、再釋放、再配置...
		if (0 == myMallocHandle) { __throwBadAlloc; }
		(*myMallocHandle)();         //呼叫處理例程,企圖釋放記憶體
		result = malloc(n);          //再次嘗試配置記憶體
		if (result)  return(result);
	}
}
  • 若記憶體不足處理函式oomMalloc與oomRealloc未設定記憶體不足處理例程,則直接丟出bad_alloc異常資訊

2.3 第二級配置器

  • 第二級配置器多了一些機制,避免小額區塊造成記憶體的碎片,減少配置時的額外負擔
  • 具體實現:
    1.如果區塊夠大,超過128bytes,移交第一級配置器處理
    2.當區塊小於128bytes,則以記憶體池管理
  1. 每次配置一塊記憶體,維護對應的自由連結串列;
  2. 下次收到相同大小的請求時,則直接從自由連結串列中拔出;
  3. 客端釋還小額區塊,就回收到free-lists中
  • 第二級配置器主動將任何小額區塊的記憶體需求上調至8的倍數(ROUND_UP函式實現)
  • 以下是部分函式實現:
//提升函式,將bytes提升至8的倍數
static size_t ROUND_UP(size_t bytes) {
       return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1));
}
//16個free-lists
static obj * volatile free_list[__NFREELISTS];
//填充free_list
static size_t FREELIST_INDEX(size_t  bytes) {
     return (((bytes)  + __ALIGN - 1)/__ALIGN  - 1);
}

以下重點理解

2.3.1 空間配置函式alllocate()

  • 步驟:
    1.判斷區塊大小,大於128bytes呼叫第一級配置器
    2.小於128bytes檢查對應的free_list,如果有可用區塊,直接拿來用;否則上調至8倍數邊界,呼叫refill為free_list重新填充

    在這裡插入圖片描述
static void * allocate(size_t n) {
  obj * volatile * 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 (result == 0) {
    void *r = refill(ROUND_UP(n));  //沒找到可用的free_list,重新填充free_list
    return r;
  }
  //調整free_list
  *my_free_list = result->free_list_link;
  return (result); 
};

2.3.2 空間釋放函式 deallocate()

  • 步驟:
    1.判斷區塊大小,大於128bytes呼叫第一級配置器
    2.小於128bytes則找出對應的free_list,將區塊回收

在這裡插入圖片描述

static void deallocate(void *p, size_t n) {
  obj *q = (obj *)p;
  obj * volatile * 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;  //調整free_list,回收區塊
  *my_free_list = q;
}

2.3.3 重新裝填free_list

  • 步驟:
    1.free list中無可用區塊,呼叫refill(),為free list填充空間
    2.新空間取自記憶體池(由chunk_alloc())完成,預設取20個新節點
    3.如果只申請到一塊,則分配給呼叫者;否則在chunk空間建立free list
template <bool threads, int inst>
void * __default_alloc_template<threads, inst>::refill(size_t n) {
	int nobjs = 20;  //預設取20塊新節點
	char * chunk = chunk_alloc(n, nobjs);   //在記憶體池中申請
	if (1 == nobjs)   return(chunk);  //只獲得一塊區塊,給呼叫者使用
	//否則跳整free list/,納入新節點
	obj * my_free_list = free_list + FREELIST_INDDEX(n);
	obj * result = (obj*)chunk; //返回一塊給客端
	obj * next = (obj*)(chunk + n); //指向下一塊記憶體
	obj * cur = next;
	*my_free_list = cur;
	//在chunk空間建立free list連線
	for (int i = 1; i < nobjs; ++i) {
		next = (obj*)((char*)next + n);
		cur->free_list_link = next;
		cur = next;
	}
	cur->free_list_link = NULL;
	return result;
}

2.3.4 記憶體池取空間(chunk_alloc實現)

  • 步驟(主要分以下四個步驟進行):
    1. 記憶體池剩餘空間完全滿足需求,則直接分配給使用者
    2. 若剩餘空間不能完全滿足,但足夠提供一個以上的區塊,則將這些區塊分配給使用者
    3. 若剩餘空間一個都滿足不了,則將殘餘空間編入free list,然後配置heap空間:

1.向系統heap申請空間
2. 分配成功則返回
3. 分配失敗則檢索自己的free list檢視是否有可用區塊
4. 到處都無記憶體使用則呼叫第一級配置器

template <bool threads, int inst>
char *
__default_alloc_template<threads, inst>::
	chunk_alloc(size_t size, int& nobjs) {
		char *result;
		size_t totalbytes = size * nobjs;  //需求量 
		size_t bytesleft = end_free - start_free;  //記憶體池剩餘量 
		//如果剩餘大於需求 
		if (bytesleft >= totalbytes) {
			result = start_free;
			start_free += totalbytes;
			resturn result;
		}
		//剩餘只能滿足一個或以上的區塊,不能全部滿足 
		else if(bytesleft >= size) {
			nobjs = bytesleft/size;
			totalbytes = size * nobjs;
			result = start_free;
			start_free += totalbytes;
			return result;
		}
		//剩餘空間一個區塊都不能滿足
		else {
			//記憶體池中還有些許零頭 ,將其分配給使用者 
			if (bytesleft >0) {
				//尋找適當的free list 
				obj * volatile *myfreelist = free_list + FREELIST_INDEX(bytesleft);
				((obj *)start_free)->free_list_link = *myfreelist;
				*myfreelist = (obj *)start_free;
			}
			size_t bytesToGet = 2 * totalbytes + ROUND_UP(heap_size>>4); //配置heap空間,初始量設為需求的兩倍加上一個隨配置次數增大的附加量 
			start_free = (char *)malloc(bytesToGet); 
			if (0 == start_free) {  
				//heap空間不足,malloc失敗
				 int i;
				 obj * volatile * myfreelist, *p;
				 //搜尋free list,查詢是否還有未用的區塊
				 for (i = size; i <= _MAX_BYTES; i += _ALIGN) {
				 	myfreelist = free_list + FREELIST_INDEX(i);
				 	p = *myfreelist;
				 	if (0 != p) {
				 		//調整free list釋放未用區塊 
				 		myfreelist = 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(bytesToGet);  
			}
			//分配成功則返回 
			heap_size = bytesToGet;
			end_free = start_free + bytesToGet;
			return (chunk_alloc(size, nobjs));   //遞迴修正nobjs 
		} 	
	}

3. C++ new-handler機制

本章在講解第一級配置器時初次提及C++ new-handler機制,於是就去《Effective C++》進一步瞭解了有關new-handler機制,日後再次遇到時也能迅速回憶起。

  • 1.“當operator new丟擲異常以反映一個未獲滿足的記憶體需求之前,它會先呼叫一個客戶指定的錯誤處理函式,一個所謂的new-handler”,這是Effective C++中的原話,用以引出new-handler,從這句話我們大概能知道new-handler是用來處理記憶體不足的一個機制
  • 2.為了指定這個“用以處理記憶體不足”的函式,客戶需要呼叫set_new_handler,定義於< new>的一個標準程式庫函式:
namespace std {
	typedef void (*new_handler)();
	//返回值和引數都是new_handler
	new_handler set_new_handler(new_handler p) throw();
}
  • 3.set_new_handler的引數是一個指標,指向operator new無法分配足夠記憶體時需要呼叫的函式:
void outOfMem()
{
	std::cerr << "Unable to satisfy request for memory\n";
	std::abort();
}
int main()
{
	std::set_new_handler(outOfMem); //當記憶體不足時,呼叫處理函式outOfMem
	int* pBigDataArrry = new int[100000000L];
	...
}
  • 4.設計一個良好的new-handler:

4.1 讓更多記憶體可被使用:程式一開始執行就分配一大塊記憶體,而後當new-handler第一次被呼叫,將它們釋放還給程式
4.2 安裝另一個new-handler:令new-handler修改一些會影響new-handler行為的資料
4.3 解除安裝new-handler將NULL指標傳給set_new_handler
4.4 丟擲bad_alloc的異常
4.5 不返回:呼叫abort或exit

  • 5.基於以上設計,你可以定製你自己的new-handle機制

4. free list的節點結構

在這裡插入圖片描述

  • 通過上圖可以瞭解到STL通過指標來維護自由連結串列,有人可能會想每個節點都需要額外的指標,會不會造成額外的負擔呢?答案是會的,那麼STL的設計精妙之處在哪呢?我們可以先來看一看它的節點結構:
union obj {
	union obj  * free_list_link;
	char client_data[1];
}
  • "上述obj所用的union,由於union之故,從其第一欄位觀之,obj可被視為一個指標,指向相同形式的另一個obj,從其第二欄位觀之,obj可被視為一個指標,指向實際區塊“,這段話前半部分很好理解,因為free_list_link本身就是一個obj的指標,但後半部分obj可被視為一個指標,指向實際區塊,這一句話又怎麼理解呢?
    1.首先我們回顧下union的性質,一個union類任意時刻只允許其一個數據成員有值,且資料成員公用記憶體
    2.由上,該free list節點結構union中的資料成員free_list_link和client_data公用一塊記憶體,前者是指向下一節點的指標,後者是指向實際記憶體的指標,無論什麼情況,二者只用其一,不分配的時候,節點就放在空閒連結串列之內,節點內容是指向下一節點的地址,如果被分配了,那麼節點指向的就是實際的記憶體