咬碎STL空間配置器

STL空間配置器c++

1、開場白:程序員

給個人感受就是,瞭解是空間配置器的功能,是那麼的明瞭;在看原理,我仍是很開心;接下來是360度大轉變:算法

       那麼長的變量或者函數命名、那麼多的宏、不爽,不過,趕上我這種二貨,是精華,我也給嚼碎了,下面開始吧:數組

2、STL是什麼:數據結構

1.STL(Standard TemplateLibrary),即標準模板庫,是一個具備工業強度的,高效的C++程序庫。多線程

2.它被容納於C++標準程序庫(C++ StandardLibrary)中,是ANSI/ISO C++標準中最新的也是極具革命性的一部分。框架

3.該庫包含了諸多在計算機科學領域裏所經常使用的基本數據結構和基本算法。爲廣大C++程序員們提供了一個可擴展的應用框架,高度體現了軟件的可複用性。函數

4.STL(Standard TemplateLibrary,標準模板庫),從根本上說,STL是一些「容器」的集合,這些「容器」有list,vector,set,map等,STL也是算法和其餘一些組件的集合。
性能

一句話:就是爲了有品味的偷懶,設計出來造福廣大碼農的。測試

3、STL空間配置器:

1>>爲何要用空間配置器:

在軟件開發,使用不少的小塊內存,在程序中動態申請,釋放。

那麼問題就來了:

1:內存碎片問題。(這裏是外碎片問題)

2:頻繁的小塊內存申請,調用malloc,系統調用產生性能問題。

另外說明:

1.內碎片:由於內存對齊、訪問效率而產生 。如 用戶須要6字節,實際獲得8或者10字節的問題,其中的碎片是浪費掉的。

2.外碎片:系統中內存總量足夠,可是不連續,因此沒法分配給用戶使用而產生的浪費。以下圖所示:


頻繁分配和 釋放事後,空白區域不在連續,當程序須要再次分配沒存時,雖然零碎的內存加起來遠遠大過程序須要申請的內存,可是,內存申請,只認連續的,那麼不連續,大多就荒廢了!

2>>怎麼用:

知道了問題,那麼空間配置器就是來解決問題的,那麼如何解決呢:下面來看看:

      在stl_alloc.h中定義了兩級配置器,主要思想是申請大塊內存池,小塊內存直接從內存池中申請,當不夠用時再申請新的內存池,還有就是大塊內存直接申請。當申請空間大於128字節時調用第一級配置器,第一級配置器沒有用operator::new和operator::delete來申請空間,而是直接調用malloc/free和realloc,而且實現了相似c++中new-handler的機制。所謂c++ new handler機制是,你能夠要求系統在內存配置需求沒法被知足時,調用一個指定的函數。換句話說,一旦::operator::new沒法完成任務,在丟出std::bad_alloc異常狀態以前,會先調用由客端指定的處理例程,該處理例程一般稱爲new-handler.new-handler解決內存作法有特定的模式。SGI第一級配置器的allocate()和realloc都是在調用malloc和realloc不成功後,改調用oom_malloc()和oom_realloc(),後二者都有內循環,不斷調用"內存不足處理例程",指望在某次調用以後,得到足夠的內存而圓滿完成任務。但若是「內存不足處理例程「並未被客端設定,oom_malloc()和oom_realloc便調用_THROW_BAD_ALLOC, 丟出bad_alloc異常信息,或利用exit(1)硬生生停止程序。
     在stl_alloc.h中定義的第二級配置器中,若是區塊夠大,超過128字節時,就移交給第一級配置器處理。當區塊小於128字節時,則之內存池管理,此法又稱爲次層配置,每次配置一大塊內存,並維護對應的自由鏈表(free-list)。下次若再有相同大小的內存需求,就直接從free-list中拔出,關於自由鏈表:


       若是客端釋還小額區塊,就由配置器回收到free-lists中,另外,配置器除了負責配置,也負責回收。爲了管理方便,SGI第二級配置器會主動將任何小額區塊的內存需求量上調至8的倍數。並維護16個free-lists,各自管理大小分別爲8,16,24,32,40,48,56,64,72,80,88,96,104, 112,120,128 字節的小額區塊。當申請小於等於128字節時就會檢查對應的free list,若是free-list中有可用的區塊,就直接拿來,若是沒有,就準備爲對應的free-list 從新填充空間。新的空間將取自內存池,缺省取得20個新節點,若是內存池不足(可是還足以配置一個以上的節點),就返回的相應的節點數.若是當內存池中連一個節點大小都不夠時,就申請新的內存池,大小爲2*total_bytes+ROUND_UP(heap_size>>4),totoal_bytes 爲申請的空間大小,ROUND_UP調整爲8的倍數,heap_size爲當前總申請內存池的大小。若是申請該內存池成功就把原來內存池中剩下的空間分配給適當的free-list.萬一山窮水盡,整個system heap空間都不夠了(以致沒法爲內存池注入源頭活水),malloc()行動失敗,就會四處尋找有無"尚有未用區塊,且區塊足夠大 "之free lists.找到了就挖一塊交出,找不到就調用第一級配置器。第一級配置器其實也是使用malloc來配置內存。但它有out-of-memory處理機制(相似new-handler機制),或許有機會釋放其餘的內存拿來此處使用。若是能夠就成功,不然發出bad_alloc異常。

       實現時,allocator須要維護一個存儲16個空閒塊列表表頭的數組free_list,數組元素i是一個指向塊大小爲8*(i+1)字節的空閒塊列表的表頭,一個指向內存池起始地址的指針start_free和一個指向結束地址的指針end_free。空閒塊列表節點的結構以下:

union obj  
{  
    union obj * free_list_link;  
    char client_data[1];  
}; 
      這個結構能夠看作是從一個內存塊中摳出4個字節大小來,當這個內存塊空閒時,它存儲了下個空閒塊,當這個內存塊交付給用戶時,它存儲的時用戶的數據。所以,allocator中的空閒塊鏈表能夠表示成:
    obj* free_list[16];

下面看看一個博友的僞代碼,寫的至關不錯,值得借鑑:

// 算法:allocate  
// 輸入:申請內存的大小size  
// 輸出:若分配成功,則返回一個內存的地址,不然返回NULL  
{  
    if(size 大於 128)  
        啓動第一級分配器直接調用malloc分配所需的內存並返回內存地址;  
    else  
    {  
        將size向上round up成8的倍數並根據大小從free_list中取對應的表頭free_list_head  
        if(free_list_head 不爲空)  
        {  
            從該列表中取下第一個空閒塊並調整free_list,返回free_list_head  
        }  
        else  
        {  
            調用refill算法創建空閒塊列表並返回所需的內存地址  
        }  
    }  
}  
  
  
// 算法:refill  
// 輸入:內存塊的大小size  
// 輸出:創建空閒塊鏈表並返回第一個可用的內存地址  
{  
    調用chunk_alloc算法分配若干個大小爲size的連續內存區域並返回起始地址chunk和成功分配的塊數nobj  
    if(塊數爲1)  
        直接返回 chunk;  
    else  
    {  
        開始在chunk地址塊中創建free_list  
        根據size取free_list中對應的表頭元素free_list_head   
        將free_list_head 指向chunk中偏移起始地址爲size的地址處,即free_list_head = (obj*)(chunk+size)  
        再將整個chunk中剩下的nobj-1個內存塊串聯起來構成一個空閒列表  
        返回chunk,即chunk中第一個空閒的內存塊  
    }  
}  
  
  
// 算法:chunk_alloc  
// 輸入:內存塊的大小size,預分配的內存塊數nobj(以引用傳遞)  
// 輸出:一塊連續的內存區域的地址和該區域內能夠容納的內存塊的塊數  
{  
    計算總共所需的內存大小total_bytes  
    if(內存池足以分配,即end_free-start_free >= total_bytes)  
    {  
        則更新start_free  
        返回舊的start_free  
    }  
    else if(內存池不夠分配nobj個內存塊,但至少能夠分配一個)  
    {  
        計算能夠分配的內存塊數並修改nobj  
        更新start_free並返回原來的start_free  
    }  
    else     // 內存池連一個內存塊都分配不了  
    {  
        先將內存池的內存塊鏈入到對應的free_list中後  
        調用malloc操做從新分配內存池,大小爲2倍的total_bytes爲附加量,start_free指向返回的內存地址  
        if(分配不成功)  
        {  
            if(16個空閒列表中尚有空閒塊)  
                嘗試將16個空閒列表中空閒塊回收到內存池中再調用chunk_alloc(size,nobj)  
            else  
                調用第一級分配器嘗試out of memory機制是否還有用  
        }  
        更新end_free爲start_free+total_bytes,heap_size爲2倍的total_bytes  
        調用chunk_alloc(size,nobj)  
    }  
}  
  
  
// 算法:deallocate  
// 輸入:須要釋放的內存塊地址p和大小size  
{  
    if(size 大於128字節)  
        直接調用free(p)釋放  
    else  
    {  
        將size向上取8的倍數,並據此獲取對應的空閒列表表頭指針free_list_head  
        調整free_list_head將p鏈入空閒列表塊中  
    }  
}  
如下爲一級和二級空間配置器源碼:

allocator.h

#pragma once

#include <stdio.h>
#include <stdarg.h>

#define __DEBUG__

static string GetFileName(const string& path)
{
	char ch = '/';

#ifdef _WIN32
	ch = '\\';
#endif

	size_t pos = path.rfind(ch);
	if (pos == string::npos)
	{
		return path;
	}
	else
	{
		return path.substr(pos + 1);
	}
}
// 用於調試追溯的trace log
inline static void __trace_debug(const char* function,
			const char* filename, int line, char* format, ...) 
{
#ifdef __DEBUG__
	// 輸出調用函數的信息
	fprintf(stdout, "【%s:%d】-%s", GetFileName(filename).c_str(), line, function);

	// 輸出用戶打的trace信息
	va_list args;
	va_start (args, format);
	vfprintf (stdout, format, args);
	va_end (args);
#endif
}

#define __TRACE_DEBUG(...)	\
	__trace_debug(__FUNCTION__, __FILE__, __LINE__, __VA_ARGS__);


////////////////////////////////////////////////////////////////////////////
// 如下是模擬實現SGI STL30版的內存配置器。

// SimpleAlloc統一封裝的內存分配的接口
template<class T, class Alloc>
class SimpleAlloc
{
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/realloc/free)
//

// 內存分配失敗之後處理的句柄handler類型
typedef void(*ALLOC_OOM_FUN)();
template <int inst>
class __MallocAllocTemplate
{
private:
	//static void (* __sMallocAllocOomHandler)();
	static ALLOC_OOM_FUN __sMallocAllocOomHandler;

	static void * OomMalloc(size_t n)
	{
		ALLOC_OOM_FUN handler;
		void* result;

		//
		// 1:分配內存成功,則直接返回
		// 2:若分配失敗,則檢查是否設置處理的handler,
		// 有則調用之後再分配。不斷重複這個過程,直到分配成功爲止。
		// 沒有設置處理的handler,則直接結束程序。
		//
		for (;;) {
			handler = __sMallocAllocOomHandler;
			if (0 == handler)
			{
				cerr<<"out of memory"<<endl;
				exit(-1);
			}

			handler();

			result = malloc(n);
			if (result)
				return(result);
		}
	}

	static void *OomRealloc(void* p, size_t n)
	{
		// 同上
		ALLOC_OOM_FUN handler;
		void* result;

		for (;;) {
			handler = __sMallocAllocOomHandler;
			if (0 == handler)
			{
				cerr<<"out of memory"<<endl;
				exit(-1);
			}

			(*handler)();
			result = realloc(p, n);
			if (result) return(result);
		}
	}
public:
	static void * Allocate(size_t n)
	{
		__TRACE_DEBUG("(n:%u)\n", n);

		void *result = malloc(n);
		if (0 == result) result = OomMalloc(n);
		return result;
	}

	static void Deallocate(void *p, size_t /* n */)
	{
		__TRACE_DEBUG("(p:%p)\n", p);

		free(p);
	}

	static void* Reallocate(void *p, size_t /* old_sz */, size_t new_sz)
	{
		void * result = realloc(p, new_sz);
		if (0 == result) result = OomRealloc(p, new_sz);
		return result;
	}

	static void (* SetMallocHandler(void (*f)()))()
	{
		void (* old)() = __sMallocAllocOomHandler;
		__sMallocAllocOomHandler = f;
		return(old);
	}
};

// 分配內存失敗處理函數的句柄函數指針
template <int inst>
ALLOC_OOM_FUN __MallocAllocTemplate<inst>::__sMallocAllocOomHandler = 0;

typedef __MallocAllocTemplate<0> MallocAlloc;

//#define __USE_MALLOC

# ifdef __USE_MALLOC
typedef __MallocAllocTemplate<0> MallocAlloc;
typedef MallocAlloc Alloc;
# else

////////////////////////////////////////////////////////////////////////
// 二級空間配置器

template <bool threads, int inst>
class __DefaultAllocTemplate
{
public:
	enum {__ALIGN = 8};				// 排列基準值(也是排列間隔)
	enum {__MAX_BYTES = 128};			// 最大值
	enum {__NFREELISTS = __MAX_BYTES/__ALIGN};	// 排列鏈大小

	static size_t ROUND_UP(size_t bytes)
	{
		// 對齊
		return ((bytes + __ALIGN - 1) & ~(__ALIGN - 1));
	}

	static size_t FREELIST_INDEX(size_t bytes)
	{
		// bytes == 9
		// bytes == 8
		// bytes == 7
		return ((bytes + __ALIGN - 1)/__ALIGN - 1);
	}

	union Obj
	{
		union Obj* _freeListLink;	// 指向下一個內存塊的指針
		char _clientData[1];    /* The client sees this.*/
	};

	static Obj* volatile _freeList[__NFREELISTS];	// 自由鏈表
	static char* _startFree;						// 內存池水位線開始
	static char* _endFree;							// 內存池水位線結束
	static size_t _heapSize;						// 從系統堆分配的總大小
	
	// 獲取大塊內存插入到自由鏈表中
	static void* Refill(size_t n);
	// 從內存池中分配大塊內存
	static char* ChunkAlloc(size_t size, int &nobjs);

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

// 初始化全局靜態對象
template <bool threads, int inst>
typename __DefaultAllocTemplate<threads, inst>::Obj* volatile __DefaultAllocTemplate<threads,inst>::_freeList[__DefaultAllocTemplate<threads, inst>::__NFREELISTS];

template <bool threads, int inst>
char* __DefaultAllocTemplate<threads, inst>::_startFree = 0;
template <bool threads, int inst>
char* __DefaultAllocTemplate<threads, inst>::_endFree = 0;
template <bool threads, int inst>
size_t __DefaultAllocTemplate<threads, inst>::_heapSize = 0;;

template <bool threads, int inst>
void* __DefaultAllocTemplate<threads, inst>::Refill(size_t n)
{
	__TRACE_DEBUG("(n:%u)\n", n);

	//
	// 分配20個n bytes的內存
	// 若是不夠則能分配多少分配多少
	//
	int nobjs = 20;
	char* chunk = ChunkAlloc(n, nobjs);

	// 若是隻分配到一塊,則直接這塊內存。
	if(nobjs == 1)
		return chunk;

	Obj* result, *cur;
	size_t index = FREELIST_INDEX(n);
	result = (Obj*)chunk;

	// 把剩餘的塊連接到自由鏈表上面
	cur = (Obj*)(chunk+n);
	_freeList[index] = cur;
	for(int i = 2; i < nobjs; ++i)
	{
		cur->_freeListLink = (Obj*)(chunk+n*i);
		cur = cur->_freeListLink;
	}

	cur->_freeListLink = NULL;
	return result;
}

template <bool threads, int inst>
char* __DefaultAllocTemplate<threads, inst>::ChunkAlloc(size_t size, int &nobjs)
{
	__TRACE_DEBUG("(size: %u, nobjs: %d)\n", size, nobjs);

	char* result;
	size_t bytesNeed = size*nobjs;
	size_t bytesLeft = _endFree - _startFree;

	//
	// 1.內存池中的內存足夠,bytesLeft>=bytesNeed,則直接從內存池中取。
	// 2.內存池中的內存不足,可是夠一個bytesLeft >= size,則直接取可以取出來。
	// 3.內存池中的內存不足,則從系統堆分配大塊內存到內存池中。
	//
	if (bytesLeft >= bytesNeed)
	{
		__TRACE_DEBUG("內存池中內存足夠分配%d個對象\n", nobjs);

		result = _startFree;
		_startFree += bytesNeed;
	}
	else if (bytesLeft >= size)
	{
		__TRACE_DEBUG("內存池中內存不夠分配%d個對象,只能分配%d個對象\n", nobjs, bytesLeft / size);
		result = _startFree;
		nobjs = bytesLeft / size;
		_startFree += nobjs*size;
	}
	else
	{
		// 若內存池中還有小塊剩餘內存,則將它頭插到合適的自由鏈表
		if (bytesLeft > 0)
		{
			size_t index = FREELIST_INDEX(bytesLeft);
			((Obj*)_startFree)->_freeListLink = _freeList[index];
			_freeList[index] = (Obj*)_startFree;
			_startFree = NULL;

			__TRACE_DEBUG("將內存池中剩餘的空間,分配給freeList[%d]\n", index);
		}

		// 從系統堆分配兩倍+已分配的heapSize/8的內存到內存池中
		size_t bytesToGet = 2*bytesNeed + ROUND_UP(_heapSize>>4);
		_startFree = (char*)malloc(bytesToGet);
		__TRACE_DEBUG("內存池空間不足,系統堆分配%u bytes內存\n", bytesToGet);

		//
		// 【無奈之舉】
		// 若是在系統堆中內存分配失敗,則嘗試到自由鏈表中更大的節點中分配
		//
		if (_startFree == NULL)
		{
			__TRACE_DEBUG("系統堆已無足夠,無奈之下,智能到自由鏈表中看看\n");

			for(int i = size; i <= __MAX_BYTES; i+=__ALIGN)
			{
				Obj* head = _freeList[FREELIST_INDEX(size)];
				if (head)
				{
					_startFree = (char*)head;
					_freeList[FREELIST_INDEX(size)] = head->_freeListLink;
					_endFree = _startFree+i;
					return ChunkAlloc(size, nobjs);
				}
			}

			//
			// 【最後一根稻草】
			// 自由鏈表中也沒有分配到內存,則再到一級配置器中分配內存,
			// 一級配置器中可能有設置的處理內存,或許能分配到內存。
			//
			__TRACE_DEBUG("系統堆和自由鏈表都已無內存,一級配置器作最後一根稻草\n");
			_startFree = (char*)MallocAlloc::Allocate(bytesToGet);
		}

		// 從系統堆分配的總字節數。(可用於下次分配時進行調節)
		_heapSize += bytesToGet;
		_endFree = _startFree + bytesToGet;

		// 遞歸調用獲取內存
		return ChunkAlloc(size, nobjs);
	}

	return result;
}

template <bool threads, int inst>
void* __DefaultAllocTemplate<threads, inst>::Allocate(size_t n)
{
	__TRACE_DEBUG("(n: %u)\n", n);

	//
	// 若 n > __MAX_BYTES則直接在一級配置器中獲取
	// 不然在二級配置器中獲取
	// 
	if (n > __MAX_BYTES)
	{
		return MallocAlloc::Allocate(n);
	}

	size_t index = FREELIST_INDEX(n);
	void* ret = NULL;

	//
	// 1.若是自由鏈表中沒有內存則經過Refill進行填充
	// 2.若是自由鏈表中有則直接返回一個節點塊內存
	// ps:多線程環境須要考慮加鎖
	//
	Obj* head = _freeList[index];
	if (head == NULL)
	{
		return Refill(ROUND_UP(n));
	}
	else
	{
		__TRACE_DEBUG("自由鏈表取內存:_freeList[%d]\n", index);

		_freeList[index] = head->_freeListLink;
		return head;
	}
}

template <bool threads, int inst>
void __DefaultAllocTemplate<threads, inst>::Deallocate(void *p, size_t n)
{
	__TRACE_DEBUG("(p:%p, n: %u)\n",p, n);


	//
	// 若 n > __MAX_BYTES則直接歸還給一級配置器
	// 不然在放回二級配置器的自由鏈表
	// 
	if (n > __MAX_BYTES)
	{
		MallocAlloc::Deallocate(p, n);
	}
	else
	{
		// ps:多線程環境須要考慮加鎖
		size_t index = FREELIST_INDEX(n);

		// 頭插回自由鏈表
		Obj* tmp = (Obj*)p;
		tmp->_freeListLink = _freeList[index];
		_freeList[index] = tmp;
	}
}

template <bool threads, int inst>
void* __DefaultAllocTemplate<threads, 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 (ROUND_UP(old_sz) == 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 __DefaultAllocTemplate<false, 0> Alloc;
#endif // __USE_MALLOC


// 經過__TRACE_DEBUG作白盒測試

// 測試內存池的一級、二級配置器功能
void Test1()
{
	// 測試調用一級配置器分配內存
	cout<<"測試調用一級配置器分配內存"<<endl;
	char*p1 = SimpleAlloc<char, Alloc>::Allocate(129);
	SimpleAlloc<char, Alloc>::Deallocate(p1, 129);

	// 測試調用二級配置器分配內存
	cout<<"測試調用二級配置器分配內存"<<endl;
	char*p2 = SimpleAlloc<char, Alloc>::Allocate(128);
	char*p3 = SimpleAlloc<char, Alloc>::Allocate(128);
	char*p4 = SimpleAlloc<char, Alloc>::Allocate(128);
	char*p5 = SimpleAlloc<char, Alloc>::Allocate(128);
	SimpleAlloc<char, Alloc>::Deallocate(p2, 128);
	SimpleAlloc<char, Alloc>::Deallocate(p3, 128);
	SimpleAlloc<char, Alloc>::Deallocate(p4, 128);
	SimpleAlloc<char, Alloc>::Deallocate(p5, 128);

	for (int i = 0; i < 21; ++i)
	{
		printf("測試第%d次分配\n", i+1);
		char*p = SimpleAlloc<char, Alloc>::Allocate(128);
	}
}

// 測試特殊場景
void Test2()
{
	cout<<"測試內存池空間不足分配20個"<<endl;
	// 8*20->8*2->320 
	char*p1 = SimpleAlloc<char, Alloc>::Allocate(8);

	char*p2 = SimpleAlloc<char, Alloc>::Allocate(8);

	cout<<"測試內存池空間不足,系統堆進行分配"<<endl;
	char*p3 = SimpleAlloc<char, Alloc>::Allocate(12);
}

// 測試系統堆內存耗盡的場景
void Test3()
{
	cout<<"測試系統堆內存耗盡"<<endl;

	SimpleAlloc<char, Alloc>::Allocate(1024*1024*1024);
	//SimpleAlloc<char, Alloc>::Allocate(1024*1024*1024);
	SimpleAlloc<char, Alloc>::Allocate(1024*1024);

	// 很差測試,說明系統管理小塊內存的能力仍是很強的。
	for (int i = 0; i < 100000; ++i)
	{
		char*p1 = SimpleAlloc<char, Alloc>::Allocate(128);
	}
}
4、討論一個問題:

對於內存池來講,這樣的大內存池子是用malloc來的,想釋放,固然用free好了,可是,對於從內存池中跑向自由鏈表之下的小塊內存,如何釋放?

其實自由鏈表中的內存既內有還給操做系統,也沒有還給內存池,在自由鏈表中,且配置器的全部方法,成員都是靜態的,那麼他們就是存放在靜態區。這些內存即在程序結束釋放。

相關文章
相關標籤/搜索