1. 程式人生 > >[SGI STL]空間配置器--記憶體管理

[SGI STL]空間配置器--記憶體管理

[SGI STL]系列文章前言

       廢話不多說,讀侯捷的SGI STL原始碼分析目的有三個:

1,接觸c++不久就開始跟STL打交道,一直有個好奇心,這麼強大的庫到底是誰、咋實現的?;

2,不熟悉實現就用不好STL,所以想更好的應用STL,就有必要一探其底層驅動;

3,引用林語堂先生的一句話:“只用一樣東西,不明白它的道理,實在不高明”;

目錄

7 小結

1,如何使用空間介面卡

其實以運用STL的角度來看,完全可以忽略空間介面卡,因為每個容器都是通過預設引數指定好了allocator,通過檢視vector的宣告可以看出:

template<class _Ty,class _Alloc = allocator<_Ty> >
	class vector
{
//...
}

如下程式碼中的vector沒有指定allocator,預設的allocator會自動根據你傳入的元素,調整記憶體空間:

#include <vector>

void main()
{
	std::vector<int> vecTemp;
	for (int i = 0;i<10;i++)
	{
		vecTemp.push_back(i);
	}

	getchar();
}

其實,完整的vecTemp宣告應該是 vetor<int, allocator<int>> vecTemp。

假如我們自定義了將記憶體分配指向磁碟或者其他儲存介質空間的allocator,那麼只要在宣告時傳入設計好的allocator,不再使用預設的allocator就行了。

那麼問題來了,怎麼樣才能設計一個allocator呢?繼續看~

2,一個標準的空間配置器

首先,設計一個空間配置器需要包含什麼介面呢?我們從如下的例子引入:

	class Foo{...};
	Foo* pFoo = new Foo;//< 第一階段,幹了倆事:1,配置記憶體 2,在配置好的記憶體上構造物件
	delete pFoo; //< 第二階段,也幹了倆事:1,析構物件 2,釋放記憶體

所以,一個allocator至少要包含四個功能:申請記憶體、構造物件、析構物件、釋放記憶體。

其次,我可以很負責人的告訴你,如果你的allocator只包含上述四個功能,肯定無法再STL中運用^_^。因為STL對allocator的組成已經規定好了,即STL規範。那麼STL中的allocator相關的規範是啥呢?我們通過一個符合STL標準的allocator(主要參考書中的JJ::allocator,略有修改)來說明:

namespace JJ 
{
	template <class T>
	class allocator
	{
	public:
		//< 七個typedef主要是為了迭代器的型別萃取,迭代器章節會提到
		typedef T		value_type;
		typedef T*		pointer;
		typedef const T*	const_pointer;
		typedef T&		reference;
		typedef const T&	const_reference;
		typedef size_t		size_type;
		typedef ptrdiff_t	difference_type;

		//< 成員模板 rebind
		//< 定義了一個associated type other,other也是一個allocator的例項,但是負責管理的物件型別與T不同
		//< 具體可以參考https://blog.csdn.net/qq100440110/article/details/50198789
		template <class U>
		struct rebind
		{
			typedef allocator<U> other;
		};

		//記憶體申請 直接使用new
		pointer allocate(size_type n, const void* hint = 0)
		{
			T* tmp = (T*)(::operator new((size_t)(n * sizeof(T))));
			if (tmp == 0)
				cerr << "out of memory" << endl;

			return tmp;
		}

		//建構函式 使用placement_new 在p處構造T1
		void construct(pointer p, const T& value)
		{
			new(p) T1(value);
		}

		//解構函式
		void destroy(pointer p)
		{
			p->~T();
		}

		//釋放記憶體  直接使用delete
		void deallocate(pointer p)
		{
			::operator delete(p);
		}

		//取地址
		pointer address(reference x)
		{
			return (pointer)&x;
		}

		//返回const物件的地址
		const_pointer const_address(const_reference x)
		{
			return (const_pointer)&x;
		}

		//可成功配置的最大量
		size_type max_size() const
		{
			return size_type(UINT_MAX / sizeof(T));
		}
	};
}// NAMESPACE_JJ_END

這樣,我們設計的第一個allocator完成了,就可以在實際中使用了:

	int ia[5] = { 1,2,3,4,5 };
	vector<int, JJ::allocator<int> > vec(ia, ia + 5);

3,SGI STL 空間配置器架構

有人可能會想,既然設計一個空間配置器這麼簡單,STL的多個毛啊,為啥它的這麼NB。其實,STL的空間配置器不只多個毛,是多很多毛,不是NB,而是很NB,從這就能看出來王者與青銅的差別了,膜拜之~

由於一個記憶體配置與釋放操作通常分兩個階段(見2中的例子),為了精密分工,STL allocator將這兩個階段的操作區分開來:

1,記憶體配置由alloc::allocate()負責,記憶體釋放由alloc::deallocate()負責;

2,物件構造由::construct()負責,物件析構由::destroy()負責。

其實,對於記憶體配置和釋放還有個allocator::allocate()和allocator::deallocate(),這是SGI定義的符合部分STL標準的配置器,但由於效率不佳,不推薦使用。其實它就是對::operator new和::operator delete做了一層薄薄的封裝。

思維導圖如下:

書中原圖:

毋庸置疑,<stl_construt.h>和<stl_alloc.h>是STL空間配置器的重頭菜,我們在這裡分別介紹。

4,構造和析構的基本工具:construct()和destroy()

先上一張書中的construt()和destroy()示意圖,對照著圖就很容易理解了:

首先,對於construct()來說很簡單了,就是接受一個指標p和一個初值value,用途就是將初值設定到指標所致的空間上,可以通過placement new來完成。

template<class T1, class T2>
void construct(T1 *p, const T2 &value)
{
	new(p) T1(value);
}

其次,從圖中可以看出destroy()有兩個版本:

第一個版本:接受一個指標(圖中的第四個),準備將所指之物析構掉。這很簡單,直接呼叫解構函式即可。

template<class T>
void destroy(T *ptr)
{
	ptr->~T();
}

第二個版本:接受一個迭代器區間,準備將這個範圍內的物件析構掉。

再講這個版本的destroy()之前,講一下trivial destructor:如果不定義解構函式,而是使用系統自帶的,也就是解構函式沒什麼作用,那麼這個解構函式稱為trivial destructor。

這裡提現了STL作者的設計亮點,他不是直接呼叫每個物件的析構,而是首先確定每個物件是否有non_rivial destructor(即自己定義了解構函式)。如果有,則呼叫物件析構,如果沒有,就什麼也不做結束。

反正思路就是上面寫的,具體可以看一下書上的程式碼,至於每個物件是否有non_rivial destructor的判斷,則用到了_type_traits<T>,會在後面講述。

圖中的第二個和第三個是第二個版本的char*和wchar*的特化。

以上就是關於construt()和destroy()的所有內容,其實還是挺簡單的。

5,空間的配置與釋放,alloc

這一節我覺得是整個SGI STL空間配置器的核心。

設計者設計了兩個配置器,準確的說是兩級配置器,兩個配置器相輔相成,相互配合最終完成空間的配置。

第一級配置器直接使用malloc()和free(),第二級則視情況採取不同的策略。而分界點是配置的記憶體是否大於128B,大於就用第一級,小於等於則通過第二級訪問複雜的memory pool整理方式。

通過是否定義_USE_MALLOC巨集,來設定是隻開啟第一級還是同時開啟第一級與第二級。SGI STL沒定義那個巨集,也就是同時開放一、二級。

5.1 第一級配置器 __malloc_alloc_template

先說一下整體的思路。就像上圖所說的,這個配置器中的allocator()直接呼叫C中的malloc(),reallocator()直接呼叫C中的realloc()。如果配置成功,則返回指標,如果不成功則呼叫out of memory處理;deallocator()直接呼叫free()。

out of memory主要呼叫使用者設定的__malloc_alloc_oom_handler,這個可以通過模擬C++中的set_new_handler()的set_malloc_handler()來設定。如果使用者指定了,則迴圈呼叫這個handler,直到分配到記憶體,如果沒定義,則拋bad_alloc異常。

具體程式碼如下:

#if 0
#include<new>
#define __THROW_BAD_ALLOC throw bad_alloc
#elif !defined(__THROW_BAD_ALLOC)
//#include<iostream.h>
#define __THROW_BAD_ALLOC cout<<"Out Of Memory."<<endl; exit(1)
#endif

//inst完全沒用
template<int inst>
class __malloc_alloc_template
{
private:
	//以下用來處理記憶體不足的情況;oom:out of memory
	static void * oom_malloc(size_t n); 
	static void * oom_realloc(void *p, size_t n);
	static void(*__malloc_alloc_oom_handler)();

public:
	static void* allocate(size_t n)
	{
		void *result = malloc(n); //< 直接呼叫malloc()
		if (result == 0)
			result = oom_malloc(n); //< 分配失敗呼叫oom_malloc()
		return result;
	}

	static void  deallocate(void *p, size_t)
	{
		free(p); //< 直接呼叫free()
	}

	static void* reallocate(void *p, size_t old_sz, size_t new_sz)
	{
		void *result = realloc(p, new_sz); //< 直接呼叫C中的realloc()
		if (0 == result)
			result = oom_realloc(p, new_sz);  //< 分配失敗呼叫oom_realloc
		return result;
	}

	//模擬C++中的set_new_handler(),也就是通過這個函式指標來指定自己的out-of-memory操作
	static void(* set_malloc_handler(void(*f)()))()
};

// 初值為0,客戶端指定
template<int inst>
void(*__malloc_alloc_template<inst>::__malloc_alloc_oom_handler)() = 0;

//如果指定了 __malloc_alloc_oom_handler,則迴圈呼叫,直到分配到記憶體,否則拋異常
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;
	}
}

//如果指定了 __malloc_alloc_oom_handler,則迴圈呼叫,直到分配到記憶體,否則拋異常
template<int inst>
void* __malloc_alloc_template<inst>::oom_realloc(void *p, 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 = realloc(p, n);
		if (result)
			return result;
	}
}

所謂的C++ new handler機制是指,你可以要求系統在記憶體配置需求無法被滿足時,呼叫一個你指定的函式。之所以要模擬這種機制,因為它並不是使用::operator new來配置記憶體的。

5.2 第二級配置器 __default_alloc_template

其實第一級配置器可以說使用者是通過new和free直接與系統記憶體打交道的,而第二級配置器相對比較複雜,大概分為3塊記憶體,簡要的溝通機制可參考下圖:

三塊空間分別為freelist、mempool、系統記憶體。

總是通過freelist來獲得記憶體,freelist如果沒有記憶體了,則呼叫refill()向mempoor獲得記憶體,mempoor如果也不夠,則呼叫trunk_alloc()向記憶體申請,記憶體都沒有呼叫第一級配置器,看看out of memory機制能夠起作用。

整個第二級配置器無非就是對上述freelist空間、mempoor空間的建立、記憶體申請、記憶體回收、以及之間的通訊。原始碼如下:

enum { __ALIGN = 8 }; //< 小型區塊的上調邊界
enum { __MAX_BYTES = 128 }; //< 小型區塊的上限
enum { __NFREELISTS = __MAX_BYTES / __ALIGN }; //< freelist個數:16個

template<bool threads, int inst>
class __default_alloc_template
{
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);

private:
	// 將申請的size上調至__ALIGN的倍數
	static size_t ROUND_UP(size_t bytes)/
	{
		return (bytes + __ALIGN - 1) & ~(__ALIGN - 1);//
	}
	
	// freelist節點結構
	union obj
	{
		union obj * free_list_link;
		char client_data[1];
	};

	// 根據要申請的區塊大小,決定使用第n號freelist,n從0算起
	static size_t FREELIST_INDEX(size_t bytes)
	{
		return (bytes + __ALIGN - 1) / __ALIGN - 1;
	}

	// 返回一個大小為n的區塊物件,並可能(通常)加入大小為n的其他區塊到freelist
	static void* refill(size_t n);

	// 配置一大塊空間,可容納nobjs個大小為size的區塊
	// 注意此處nobjs是引用,如果配置有所不便(記憶體不夠),nobjs會降低
	static char* chunk_alloc(size_t size, int &nobjs);

	static obj * free_list[__NFREELISTS]; //< 16個freelist 
	static char *start_free;//< 記憶體池其實位置
	static char *end_free; //< 記憶體池結束位置
	static size_t heap_size; //< 配置記憶體的附加量
};

// 賦初值
template<bool threads, int inst>
char* __default_alloc_template<threads, inst>::start_free = 0;
template<bool threads, int inst>
char* __default_alloc_template<threads, inst>::end_free = 0;
template<bool threads, int inst>
size_t __default_alloc_template<threads, inst>::heap_size = 0;

template<bool threads, int inst>
__default_alloc_template<threads, inst>::obj*
__default_alloc_template<threads, inst>::free_list[__NFREELISTS] =
{ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 };

5.2.1 freelist

簡單說,就是16個8byte倍數但小於128byte的連結串列,連結串列的節點如下:

union obj
{
    union obj* free_list_link;
    char client_data[1];
};

其實我對這個節點的定義還是有點疑問的,具體請看我的另一篇部落格:

一共16個freelist,每個freelist就是一個連結串列,16個的區別就是連結串列節點所佔空間大小不一樣,:

對於每個freelist的空間申請與釋放,其實就是一些連結串列的操作。

5.2.2 二級配置器中的空間配置函式allocator()

以下程式碼描述瞭如何利用二級配置器中的allocator()配置空間,以及freelist空間如何與mempool之間通訊,程式碼如下:

static void* allocate(size_t n)
{
	obj* volatile* my_free_list;
	void* result = 0;

	//如果大於128B, 直接呼叫一級配置器
	if (n > (size_t)_MAX_BYTES) 
	{
		return (malloc_alloc::allocate(n));
	}
	//尋找 16個free-list 中的一個
	my_free_list = free_list + FREELIST_INDEX(n);
	result = *__my_free_list;
	if (result == 0)
	{
		//如果freelist上沒有可用空間,則將空間調整至8的倍數
        //呼叫refill,向mempool申請記憶體,重新填充該freelist
		result = refill(ROUND_UP(n));
		return result;
	}
	else 
	{
		*my_free_list = result->_M_free_list_link;
	}

	return result;
};

其中freelist與mempool之間的通訊函式refill(),原始碼如下:

template<bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n)
{
	//預設取20個新節點連線到freelist上(其實是19個,第一個返回給使用者)
	int nobjs = 20;

	//呼叫chunk_alloc(),嘗試取得nobjs個區塊作為freelist的新節點
	//注意此處引數nobjs是通過引用傳入,有可能變小
	char* chunk = chunk_alloc(n, nobjs);
	obj* volatile* my_free_list;
	obj * result;
	obj * current_obj, *next_obj;
	int i;

	//如果只獲得一個區塊,則將這個區塊直接反饋,freelist無新節點
	if (1 == nobjs)
	{
		return chunk;
	}

	//找到需要填充的連結串列的位置
	my_free_list = freeList + FREELIST_INDEX(n);
	result = (obj*)chunk;//第一塊返回給客戶端
	//引導freelist指向新的空間
	*my_free_list = next_obj = (obj*)(chunk + n);//這裡把第二塊先掛到指標陣列對應位置下  //注意這裡的n在傳引數時已經調整到8的倍數
	for (i = 1;; i++) {//從1開始,0返回給客戶端
		cur_obj = next_obj;
		next_obj = (obj*)((chat*)next_obj + n);
		if (nobjs - 1 == i) {                   //因為第一次從記憶體池取下的空間在物理上是連續的 尾插方便用 以後用完還回自由連結串列的就不是了
			cur_obj->free_list_link = NULL;//這裡沒有新增節點
			break;
		}
		else {
			cur_obj->free_list_link = next_obj;//nobjs - 2是最後一次新增節點
		}
	}
	return result;
}

5.2.3 空間釋放函式deallocate()

如果釋放的空間大於128b則呼叫第一級配置器,如果小於128b,則將要釋放的空間連結到對應的freelist上,也就是一個在連結串列頭插入節點的過程:

static void deallocate(void* p, size_t n)
{
	obj* volatile*  my_free_list;
	obj* q = (obj*)p;

	//如果大於128,呼叫第一級配置器
	if (n > (size_t)_MAX_BYTES)
	{
		malloc_alloc::deallocate(p, n);
		return;
	}	
		
	//尋找對應的freelist
	my_free_list = _S_free_list + _S_freelist_index(n);
	//回收該區塊
	q->free_list_link = *my_free_list;
	*my_free_list = q;
}

5.2.4 記憶體池(mem pool)

chunk_alloc()是負責mem pool與系統記憶體打交道的,原始碼如下:

template<bool threads, int inst>
void* __default_alloc_template<threads, 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) 
	{
		// 記憶體池剩餘空間不能完全滿足需求量,但足夠供應一個(含)以上的區塊
		nobjs = bytes_left / size;
		total_bytes = size * nobjs;
		result = start_free;
		start_free += total_bytes;
		return result;
	}
	else 
	{
		// 記憶體池剩餘空間連一個區塊的大小都無法提供
		size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);
		// 以下試著讓記憶體池中的殘餘零頭還有利用價值
		if (bytes_left > 0) 
		{
			// 記憶體池內還有一些零頭,先配給適當的free list
			// 首先尋找適當的free list
			obj * volatile * my_free_list = free_list + FREELIST_INDEX(bytes_left);
			// 調整free list,將記憶體池中的殘餘空間編入
			((obj *)start_free)->free_list_link = *my_free_list;
			*my_free_list = (obj *)start_free;
		}

		// 配置heap空間,用來補充記憶體池
		start_free = (char *)malloc(bytes_to_get);
		if (0 == start_free) 
		{
			// heap空間不足,malloc失敗
			int i;
			obj * volatile * my_free_list, *p;
			// 試著檢視我們手上擁有的東西,這不會造成傷害。我們不打算嘗試配置
			// 較小的區塊,因為那在多程序機器上容器導致災難
			// 以下搜尋適當的free list
			// 所謂適當是指“尚未用區塊,且區塊夠大”的free list
			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內尚有未用塊
							  // 調整free list以釋放未用區塊
					*my_free_list = p->free_list_link;
					start_free = (char *)p;
					end_free = start_free + i;
					// 遞迴呼叫自己,為了修正nobjs
					return chunk_alloc(size, nobjs);
					// 注意,任何殘餘零頭終將被編入適當的free list中備用
				}
			}
			end_free = 0; // 如果出現意外,呼叫第一級配置器,看看oom機制能否盡力
			start_free = (char *)malloc_alloc::allocate(bytes_to_get);
			// 這會丟擲異常 或 記憶體不足的情況得到改善
		}
		heap_size += bytes_to_get;
		end_free = start_free + bytes_to_get;
		// 遞迴呼叫自己,為了修正nobjs
		return chunk_alloc(size, nobjs);
	}
}

我覺得書上舉的例子對這段程式碼的解釋再合適不過了,非常透徹:

5.2.5 第二級配置器總的流程框圖

自己懶得畫了摘了一個:

6 記憶體基本處理工具

提供的三個工具uninitialized_copy()、uninitialized_fill()、uninitialized_fill_n(),用於將記憶體的配置與物件的構造分別開來。如何分開的呢?我們先看一下對於一個全區間的建構函式,如何構造物件的:

1,配置記憶體區塊,足以包含範圍內的所有元素;

2,在記憶體上構造物件;

那麼這三個函式是如何發揮作用的呢?這裡用到了is_POD_type的概念。POD意指Plain Old Data,也就是標量型別或傳統的C struct型別。POD必然擁有trivial ctor/dtor/copy/assignment函式,因此我們可以:

對POD型別採取最有效的初值填寫法,如:

int a;
a = 5;

而對non-POD型別採取最保險的安全做法:

char* p = new char;
new(p) char(5);

至於怎麼判斷一個迭代器所指物件的型別,那就是利用__type_trait了,後續再說。

如果is_POD_type是__true_type,那麼這幾個工具就呼叫相應的演算法copy()、fill()、fill_n()。如果是__false_type則呼叫第4節提到的construct()。

7 小結

花了三天晚上看書,加上一個週末的下午+晚上串聯思想與寫這篇部落格,總體感覺收穫還是蠻多的,對於STL的記憶體配置以及泛型變成都有了一定得了解,還可以~