STL-空間配置器剖析

         網上有很多對於STL空間配置器源碼的剖析,之所以這麼多人去剖析空間配置器,我覺得是真的設計的太好,而且剖析空間配置器的架構的設計對於C++學者來說是一個不錯的提高能力的項目,所以加入到這個解剖大軍中來。

      參照了侯捷的《STL源碼剖析》,原本直接看源碼不懂得東西,突然間豁然開朗。再次寫下自己對於STL空間配置器的一點點理解。


   要了解空間配置器,有一張圖是必看的:


    這張圖是一級空間配置器宇二級空間配置器的封裝方式與調用。從此圖我們們可以看到其實空間配置器是分爲兩級的,而這裏所謂的兩級並沒有高低之分,它們之間的區別就是看你想要申請內存空間的大小。如果申請的內存大小超過128,那麼空間配置器就自動調用一級空間配置器。反之調用二級空間配置器。而且在這裏要說明的是空間配置器默認使用的是一級空間配置器。

 一.   一級空間配置器:

   一級空間配置器就比較簡單了,STL源碼中的一級空間配置器命名爲class __malloc_alloc_template ,它很簡單,就是對malloc,free,realloc等系統分配函數的一層封裝,我向這也是爲什麼這麼取名的原因。

     源碼中的一級空間配置器也不難看懂,懂了他的思想也就不難寫出如下的代碼:   

<span style="font-family:Microsoft YaHei;font-size:14px;">template<int inst>//非類型模板參數
class MallocAllocTemplate//一級空間配置器(malloc,free,realloc)
{
public:
	static void* Allocate(size_t n)
	{
		void* ret = malloc(n);
		if (0 == ret)
			ret = OomMalloc(n);
		return ret;
	}
	static void Deallocate(void* p)
	{
		free(p);
	}
	static void* Reallocate(void* p, size_t newsize)
	{
		void* ret = realloc(p, newsize);
		if (ret == 0)
			ret = OomRealloc(p, newsize);
		return ret;
	}
private:
	static void* OomMalloc(size_t n)//調用自定義的句柄處理函數釋放並分配內存
	{
		ALLOC_FUN hander;
		void* ret;
		while (1)
		{
			hander = MallocAllocHander;
			if (0 == hander)
			{
				cout << "Out of memory" << endl;
				exit(-1);
			}
			hander();
			ret = malloc(n);
			if (ret)
			{
				rteurn (ret);
			}
		}
	}
	static void* OomRealloc(void* p, size_t newsize)//同上
	{
		ALLOC_FUN hander;
		void* ret;
		while (1)
		{
			hander = MallocAllocHander;
			if (0 == hander)
			{
				cout << "Out of memory" << endl;
				exit(-1);
			}
			hander();
			ret = realloc(p,newsize);
			if (ret)
			{
				rteurn(ret);
			}
		}
	}
	static void(*SetMallocHandler(void(*f)()))();//設置操作系統分配內存失敗時的句柄處理函數
	static ALLOC_FUN MallocAllocHander;

};
template<int inst>
ALLOC_FUN MallocAllocTemplate<inst>::MallocAllocHander = 0;//句柄函數初始化爲0</span>

       一級空間配置器中沒有可以討論的,除了這個句柄函數:static void(*SetMallocHandler(void(*f)()))();對於一個C初學者來說想要看懂這個聲明有點難度,這是一個返回值,參數都爲函數指針的一個函數指針。填起來有點繞,其實他就是一個函數指針,它指向的是一個句柄函數,這個句柄函數對於一級空間配置器是比較重要的。

        malloc,free,realloc等庫函數是向系統申請內存並且操作的函數。平時我們並不太會遇到內存空間分配不出來的情況,但是如果這一套程序是運行在服務器上的,各種各樣的進程都需要內存。這樣頻繁的分配內存,終有一個時候,服務器再也分配不出內存,那麼空間配置器該怎麼辦呢?這個函數指針指向的句柄函數就是處理這種情況的設計。

       MallocAllocHander()一般是自己設計的一種策略。這種策略想要幫助操作系統得到內存空間用以分配。所以,設計這個函數就是一個提升空間配置器效率的一個方法。一般是大牛去玩兒的。哈哈。如果並不像設計這個策略,就把句柄函數初始化爲0.



二.    二級空間配置器:

    一級空間配置器說起來比較乏味,他只是一層系統函數封裝,真正酸爽的是二級空間配置器,裏面有很多很棒的設計。多的不說,先來看二級空間配置器的框架,上代碼:

<span style="font-family:Microsoft YaHei;font-size:14px;">template<bool threads,int inst>
class DefaultAllocTemplate//二級空間配置器
{
private:
	enum{ ALIGN = 8 };
	enum{ MAX_BYTES = 128 };
	enum{ FREELISTSIZE = MAX_BYTES / ALIGN };
public:
	static void* Allocate(size_t n)
	{
		if (n > MAX_BYTES)
		{
			return MallocAllocTemplate<inst>::Allocate(n);
		}
		void* ret = NULL;
		size_t index = GetFreeListIndex(n);

		if (FreeList[index])//自由鏈表上有內存塊
		{
			obj* cur = FreeList[index];
			ret = cur;
			FreeList[index] = cur->listLink;
		}
		else   //調用refill從內存池填充自由鏈表並返回內存池的第一個內存塊
		{
			size_t bytes = GetRoundUpNum(n);
			return Refill(bytes);
		}
		return ret;
	}
	static void* Reallocate(void* p, size_t oldsize, size_t newsize)
	{
		void* ret = NULL;
		if (oldsize > (size_t)MAX_BYTES&&newsize > (size_t)MAX_BYTES)
			return (realloc(p, newsize));
		if (GetRoundUpNum(oldsize) == GetRoundUpNum(newsize))
			return p;
		ret = Allocate(newsize);
		size_t copysize = oldsize > newsize ? newsize : oldsize;
		memcopy(ret, p, copysize);
		DeAllocate(p, oldsize);
		return ret;
	}
	static void Deallocate(void* p, size_t n)
	{
		if (n > MAX_BYTES)//如果大於MAX_BYTES直接交還給一級空間配置器釋放
			return MallocAllocTemplate<inst>::Deallocate(p, n);
		else//放回二級空間配置器的自由鏈表
		{
			size_t index = GetFreeListIndex(n);
			obj* tmp = (obj*)p;
			tmp->listLink = FreeList[index];
			Freelist[index] = tmp;
		}
	}
public:
	union obj
	{
		union obj* listLink;//自由鏈表中指向下一個內存快的指針
		char clientData[1];//調試用
	};
	static size_t GetFreeListIndex(size_t bytes)//得到所需內存塊在自由鏈表中的下標
	{
		return ((bytes + ALIGN - 1) / ALIGN - 1);
	}
	static size_t GetRoundUpNum(size_t bytes)//得到內存塊大小的向上對齊數
	{
		return (bytes + ALIGN - 1)&~(ALIGN - 1);
	}

	static void* Refill(size_t n)//從內存池拿出內存填充自由鏈表
	{
		int nobjs = 20;//申請20個n大小的內存塊
		char* chunk = ChunkAlloc(n, nobjs);
		if (nobj == 1)//只分配到一個內存
		{
			return chunk;
		}
		obj* ret = NULL;
		obj* cur = NULL;
		size_t index = GetFreeListIndex(n);
		ret = (obj*)chunk;
		cur = (obj*)(chunk + n);

		//將nobj-2個內存塊掛到自由鏈表上
		FreeList[index] = cur;
		for (int i = 2; i < nobjs; ++i)
		{
			cur->listLink = (obj*)(chunk + n*i);
			cur = cur->listLink;
		}
		cur->listLink = NULL;
		return ret;
	}
	static char* ChunkAlloc(size_t size, int& nobjs)
	{
		char* ret = NULL;
		size_t Leftbytes = endFree - startFree;
		size_t Needbytes = size * nobjs;
		if (Leftbytes >= Needbytes)
		{
			ret = startFree;
			startFree += Needbytes;
		}
		else if (Leftbytes >= size)//至少能分配到uoge內存塊
		{
			ret = startFree;
			nobjs = Leftbytes / size;
			startFree += nobjs*size;
		}
		else     //一個內存塊都分配不出來
		{
			if (Leftbytes > 0)
			{
				size_t index = GetFreeListIndex(Leftbytes);
				((obj*)startFree)->listLink = FreeList[index];
				FreeList[index] = (obj*)startFree;
				startFree = NULL;
			}
			//向操作系統申請2倍Needbytes加上已分配的heapsize/8的內存到內存池
			size_t getBytes = 2 * Needbytes + GetRoundUpNum(heapSize >> 4);
			startFree = (char*)malloc(getBytes);
			if (startFree == NULL)//從系統堆中分配內存失敗
			{
				for (int i = size; i < MAX_BYTES; i += ALIGN)
				{
					obj* head = FreeList[GetFreeListIndex(i)];
					if (head)
					{
						startFree = (char*)head;
						head = head->listLink;
						endFree = startFree + i;
						return ChunkAlloc(size, nobjs);
					}
				}
				//最後的一根救命稻草,找一級空間配置器分配內存
				//(其他進程歸還內存,調用自定義的句柄處理函數釋放內存)
				startFree = MallocAllocTemplate<inst>::Allocate(getBytes);
			}
			heapSize += getBytes;//從系統堆分配的總字節數(可以用於下次分配時進行調節)
			endFree = startFree + getBytes;

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

	static obj* volatile FreeList[FREELISTSIZE];
	static char* startFree;
	static char* endFree;
	static size_t heapSize;

};


//typename表示DefaultAllocTemplate<threads, inst>是一個類型,
//如果不標識,編譯器對此模板一無所知
template<bool threads, int inst>
typename DefaultAllocTemplate<threads, inst>::obj* volatile   
           DefaultAllocTemplate<threads, inst>::FreeList[FREELISTSIZE] = { 0 };

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;</span>

    這個代碼是我自己提取出來的源碼框架,但是他已經可以實現所有的功能。

   首先需要說明的是二級空間配置器是由一個內存池自由鏈表配合實現的

<span style="font-family:Microsoft YaHei;font-size:14px;">	static obj* volatile FreeList[FREELISTSIZE];//維護自由鏈表
	static char* startFree;//維護內存池
	static char* endFree;</span>

     srartFree就相當於水位線的一種東西,它標誌着內存池的大小。

     自由鏈表中其實是一個大小爲16的指針數組,間隔爲8的倍數。各自管理大小分別爲8,16,24,32,40,48,56,64,72,80,88,96,104, 112,120,128 字節的小額區塊。在每個下標下掛着一個鏈表,把同樣大小的內存塊鏈接在一起。此處特別像哈希桶。

自由鏈表結構:

<span style="font-family:Microsoft YaHei;font-size:14px;">	union obj
	{
		union obj* listLink;//自由鏈表中指向下一個內存快的指針
		char clientData[1];//調試用
	};</span>

     這個結構可以看做是從一個內存塊中摳出4個字節大小來,當這個內存塊空閒時,它存儲了下個空閒塊,當這個內存塊交付給用戶時,它存儲的時用戶的數據。因此,allocator中的空閒塊鏈表可以表示成:
    obj* free_list[16];

     obj* 是4個字節那麼大,但是大部分內存塊大於4。我們想要做的只是將一塊塊內存鏈接起來,我們不用看到內存裏所有的東西,所以我們可以只用強轉爲obj*就可以實現大內存塊的鏈接。


   二級空間配置器是爲頻繁分配小內存而生的一種算法。其實就是消除一級空間配置器的外碎片問題


操作系統頻繁分配內存和回收內存的時候。這些6M,4M的小內存無法利用造成了外部碎片。


二級空間配置器就比較複雜了,現在我們來分析他的那些重要的函數:

 Allocate()中:

<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="white-space:pre">	</span>static size_t GetFreeListIndex(size_t bytes)//得到所需內存塊在自由鏈表中的下標
<span style="white-space:pre">	</span>{
<span style="white-space:pre">		</span>return ((bytes + ALIGN - 1) / ALIGN - 1);
<span style="white-space:pre">	</span>}</span>
    此函數和源碼中的FREELIST_INDEX(n)是一樣的,它就是找到需要分配的內存塊在自由鏈表中的什麼地方,它的實現是((bytes + ALIGN - 1) / ALIGN - 1)。它其實是把藥分配的內存大小提升一個數量級(+7,每間隔8爲一個數量級),然後除以8,可以算到要找的內存塊下標的下一個下標,減一,剛好久找到合適的下標處,取出一塊內存塊。

<span style="font-family:Microsoft YaHei;font-size:14px;">	static size_t GetRoundUpNum(size_t bytes)//得到內存塊大小的向上對齊數
	{
		return (bytes + ALIGN - 1)&~(ALIGN - 1);
	}</span>

      此函數是得到所需內存塊大小的向上對齊數。在自由鏈表中,我們的內存塊大小總是8的倍數,但是並不是每次所需內存大小都是8的倍數。所以我們就要取比所需大小大或相等的內存塊,這就是向上取整。&~(ALIGN - 1)相當於將低8位置0,只取高8位,高8位總是8的倍數,正好符合題意。


Allocate中最重要的兩個函數static void* Refill(size_t n)和static char* ChunkAlloc(size_t size, int& nobjs):

<span style="font-family:Microsoft YaHei;font-size:14px;">static void* Refill(size_t n)//從內存池拿出內存填充自由鏈表
	{
		int nobjs = 20;//申請20個n大小的內存塊
		char* chunk = ChunkAlloc(n, nobjs);
		if (nobj == 1)//只分配到一個內存
		{
			return chunk;
		}
		obj* ret = NULL;
		obj* cur = NULL;
		size_t index = GetFreeListIndex(n);
		ret = (obj*)chunk;
		cur = (obj*)(chunk + n);

		//將nobj-2個內存塊掛到自由鏈表上
		FreeList[index] = cur;
		for (int i = 2; i < nobjs; ++i)
		{
			cur->listLink = (obj*)(chunk + n*i);
			cur = cur->listLink;
		}
		cur->listLink = NULL;
		return ret;
	}</span>

     當在自由鏈表的下標處沒有內存塊時,我們就必須調用refill去填充自由鏈表。申請時一般一次性申請20個內存塊大小的內存。通過移動startFree指針將內存池內的一段內存給“切割”出來,然後按照大小切成小塊掛在自由鏈表下面。。返回第一塊內存塊給用戶,其餘的都掛在自由鏈表下,方便下次分配,根據局部性原理,這將極大地提升了分配內存空間的效率

<span style="font-family:Microsoft YaHei;font-size:14px;">static char* ChunkAlloc(size_t size, int& nobjs)
	{
		char* ret = NULL;
		size_t Leftbytes = endFree - startFree;
		size_t Needbytes = size * nobjs;
		if (Leftbytes >= Needbytes)
		{
			ret = startFree;
			startFree += Needbytes;
		}
		else if (Leftbytes >= size)//至少能分配到一個內存塊
		{
			ret = startFree;
			nobjs = Leftbytes / size;
			startFree += nobjs*size;
		}
		else     //一個內存塊都分配不出來
		{
			if (Leftbytes > 0)
			{
				size_t index = GetFreeListIndex(Leftbytes);
				((obj*)startFree)->listLink = FreeList[index];
				FreeList[index] = (obj*)startFree;
				startFree = NULL;
			}
			//向操作系統申請2倍Needbytes加上已分配的heapsize/8的內存到內存池
			size_t getBytes = 2 * Needbytes + GetRoundUpNum(heapSize >> 4);
			startFree = (char*)malloc(getBytes);
			if (startFree == NULL)//從系統堆中分配內存失敗
			{
				for (int i = size; i < MAX_BYTES; i += ALIGN)
				{
					obj* head = FreeList[GetFreeListIndex(i)];
					if (head)
					{
						startFree = (char*)head;
						head = head->listLink;
						endFree = startFree + i;
						return ChunkAlloc(size, nobjs);
					}
				}
				//最後的一根救命稻草,找一級空間配置器分配內存
				//(其他進程歸還內存,調用自定義的句柄處理函數釋放內存)
				startFree = MallocAllocTemplate<inst>::Allocate(getBytes);
			}
			heapSize += getBytes;//從系統堆分配的總字節數(可以用於下次分配時進行調節)
			endFree = startFree + getBytes;

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


ChunkAlloc要做的就是去找操作系統要內存,依次性要20個,但是我們要考慮很多情況:

  1. 內存池裏有足夠20塊大的內存
  2. 內存池裏有小於20塊大於等於1塊的內存大小
  3. 內存池裏1塊內存那麼大都沒有
STL是這樣做的:     如果有足夠的內存,那麼一次性就給20塊,返回第一塊給用戶,其餘的掛在自由鏈表上。
                               只有一塊或者多塊,返回一塊給用戶。
                               沒有內存了,找操作系統要。
                               操作系統沒有了,啓用最後一根救命稻草,調用一級空間配置器,通過句柄函數釋放內存,分配內存。

這個就是二級空間配置器的主要邏輯結構。


還有要說明的幾點就是:
  1. 空間配置器裏所有的成員都是靜態的。是爲了在外面通過作用域就可以調用,而不需要構造對象。
  2. 空間配置器可以使用於大部分的數據結構,如List,vector等。
  3. 對於自由鏈表的初始化時特別容易錯的。
    template<bool threads, int inst>
    typename DefaultAllocTemplate<threads, inst>::obj* volatile  
            DefaultAllocTemplate<threads, inst>::FreeList[FREELISTSIZE] = { 0 };
    注意到typename了嗎。它就爲了完成一個功能,到蘇編譯器DefaultAllocTemplate<threads, inst>是一個類型,不然會出現如下錯誤:



這是難以發現的錯誤,編譯器認識DefaultAllocTemplate<threads, inst>,報一個搞不懂的錯誤。

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