【C++內存管理】G2.9 std::alloc 源碼分析

上一篇 中,對 std::alloc 的運行時狀態進行了分析,這篇再結合上一篇,深入代碼分析。


常量定義
enum { __ALIGN = 8 };                        // block 的間隔
enum { __MAX_BYTES = 128 };                  // block 的最大 bytes
enum { __NFREELISTS = __MAX_BYTES / __ALIGN }; //free-lists 的個數

這裏它用的是 enum 定義常量,可以換成 const 定義。


類結構
//模板類,第一個參數與線程有關,這裏不做討論
//第二個參數與分配器也沒有關係
template<bool thread, int inst> 
class __default_alloc_template
{
private:
	//字節對齊,向上取整
	static size_t ROUND_UP(size_t bytes);

	union obj
	{
		union *free_list_link;
	};//embaded pointer,這裏也可以用 strut

	//free_list 數組,每一個位置上掛載不同長度 block 的鏈表
	static obj* volatile free_list[__NFREELISTS];

	//根據字節數,求在 free_list 數組中的編號。
	//比如說傳入 bytes = 20,則對應於 free_list 數組中的 #2 位置,返回2。
	static size_t FREELIST_INDEX(size_t bytes);

	//向操作系統索取新的內存塊
	static void* refill(size_t n);

	static char* chunk_alloc(size_t size, int& nobjs);

	//指向 memory pool 的開始位置
	static char* start_free;

	//指向 memory pool 的結束位置
	static char* end_free;

	//總共分配的內存塊的字節數
	static size_t heap_size;

public:

	static void* allocate(size_t n);

	static void deallocate(void *p, size_t n);
};

typedef __default_alloc_template<false, 0> alloc;
  • 所有的成員函數和成員變量都是靜態的
  • std::alloc 就是 __default_alloc_template<false, 0> 的別名

簡單的工具函數

先來看看 ROUND_UP

template<bool thread, int inst>
size_t __default_alloc_template<thread, inst>::ROUND_UP(size_t bytes)
{
	return (bytes + __ALIGN - 1) & ~(__ALIGN - 1);
}

這個函數就是對客戶端傳入的字節數向 __ALIGN 取整。計算方法也很好理解:
在這裏插入圖片描述

  • bytes + __ALIGN - 1 ,有零頭存在的時候,就會前加一個 _ALGN,否則就不會加。
  • 剩餘的零頭,會在與 ~(__ALIGN - 1)& 的時候置零。

再看看 FREELIST_INDEX

template<bool thread, int inst>
size_t __default_alloc_template<thread, inst>::FREELIST_INDEX(size_t bytes) {
    return (((bytes) + __ALIGN-1)/__ALIGN - 1);
}

這個函數就是對 bytes 長度的 block 求取它對應的在 free_list 數組中的位置。結合上面的 ROUND_UP,應該也很好理解。比如說,傳入 bytes = 20,那麼它對應的就是在 #2 鏈表上。


allocate 和 deallocate

對於客戶端來說,調用的就是 allocatedeallocate 接口獲取內存塊和釋放內存塊了。先來看看 allocate 函數:

template<bool thread, int inst>
void * __default_alloc_template<thread, inst>::allocate(size_t n)
{
	//當用戶需要獲得的內存塊大小大於最大的 block 大小,即 128 時,
	//這種內存管理方式就會失效,轉而調用另外一個接口,該接口可爲用戶提供
	//類似的 new_handler 機制。這裏不做過多討論
	if (n > (size_t)__MAX_BYTES)
	{
		return malloc_allocate(n);
	}

	// 獲得 n 對應的 block 所在鏈表的頭指針的指針
	obj **my_free_list = free_list + FREELIST_INDEX(n);

	//頭指針
	obj *result = *my_free_list;

	//所在鏈表沒有空的 block 可以使用,需要重新分配內存
	if (result == nullptr)
	{
		return refill(ROUND_UP(n));
	}

	*my_free_list = result->free_list_link;

	return result;
}
  • std::alloc 中其實有兩種內存分配方式。當 傳入的字節數 n 大於 free_list 數組中的最大 block 數,比如這裏的 128,當前的內存分配方式就會失效,就會轉而調用另外一種內存分配方式,這裏不做討論。
  • 如果 n 對應的鏈表不爲空,證明該鏈表還有空閒 block 可供使用,直接返回鏈表頭指針即可,並讓頭指針指向下一個 block 位置。
  • 如果鏈表爲空,表明需要爲這一條鏈表重新分配內存,由函數 refill 完成。

deallocate 函數的實現如下:

template<bool thread, int inst>
void __default_alloc_template<thread, inst>::deallocate(void * p, size_t n)
{
	//傳入的這一塊內存並不是由當前的內存分配機制分配出去的
	if (n > (size_t)__MAX_BYTES)
	{
		malloc_deallocate(p, n);
		return;
	}

	// 獲得 n 對應的 block 所在鏈表的頭指針的指針
	obj **my_free_list = free_list + FREELIST_INDEX(n);
	obj *q = (obj*)p;

	//插入到所在鏈表的頭節點位置
	q->free_list_link = *my_free_list;
	*my_free_list = q;
}
  • 先要判斷傳入的這一塊內存是不是由當前的這種內存分配機制分配出去的。也就是判斷傳入的字節大小是不是不大於最大的 block 大小,即 __MAX_BYTES,如果不是,說明傳入的這一塊內存是由第一種內存分配方式分配出去的,就轉而調用第一種內存分配方式相對應的內存釋放函數。
  • 其他的動作就很簡單的。把傳入的內存塊插入到對應鏈表的頭節點位置就可以了。

重要輔助函數 refill 和 chunk_alloc

allocate 函數中,如果索要的對應 block 的鏈表爲空,就需要調用 refill 函數,爲該 block 所在鏈表重新分配內存塊。看看 refill 函數執行了哪些動作:

//傳入的已經是對齊之後的字節大小
template<bool thread, int inst>
void * __default_alloc_template<thread, inst>::refill(size_t n)
{
	//表示鏈表所能鏈接的最長長度
	int nobjs = 20;

	//獲取一塊內存 chunk,注意 nobj 是 pass by reference
	char *chunk = chunk_alloc(n, nobjs);

	//返回的內存塊只有一個 block 大小,則可以直接作爲結果返回
	//不需要進行後續的鏈接動作
	if (nobjs == 1)
		return chunk;

	obj **my_free_list = free_list + FREELIST_INDEX(n);

	//將獲得的內存塊的頭指針直接作爲結果返回
	obj	*result = (obj*)chunk;

	//把鏈表掛在數組 free_list 上
	*my_free_list = (obj*)(chunk + n);

	obj *next_obj = *my_free_list;
	obj* current_obj;

	//將這一塊內存鏈接爲 block 鏈表
	for (int i = 1; ; ++i)
	{
		current_obj = next_obj;

		//鏈表最後一個
		if (i == nobjs - 1)
		{
			current_obj->free_list_link = nullptr;
			break;
		}
		else
		{
			next_obj = (obj*)((char*)current_obj + n);
			current_obj->free_list_link = next_obj;
		}
	}

	return result;
}
  • 這裏應該將 20 設置爲一個 const 常量更好
  • 調用了 chunk_alloc 函數,用來獲取一塊內存。如果這塊內存的大小剛好只有一個 block 的大小,就可以直接將這塊內存返回,而不需要進行後續的鏈接動作。注意 nobjs 是以 pass by reference 的方式傳遞,表明獲得的內存塊的實際的 block size (<= 20)。
  • 將獲得的內存塊的第一塊 block 直接返回給客戶,從第二塊 block 開始鏈接,所以代碼中從 i = 1 開始。

再來看看 chunk_alloc 函數:
template<bool thread, int inst>
char * __default_alloc_template<thread, inst>::chunk_alloc(size_t size, int & nobjs)
{
	//所需要的內存塊大小
	size_t total_bytes = size * nobjs;

	//memory pool 剩餘的內存塊大小
	size_t bytes_left = end_free - start_free;

	char* result;

	//memory pool 足夠分配 nobjs 個 size 大小的 block
	if (bytes_left >= total_bytes)
	{
		result = start_free;
		start_free += total_bytes;
		return result;
	}

	//可以分至少一個 block
	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);

		//如果存在內存碎片,以該內存碎片大小爲 一個 block 大小,插入到對應的鏈表頭節點位置
		if (bytes_left > 0)
		{
			//找到內存碎片大小對應的 free_list 中的位置
			obj **my_free_list = free_list + FREELIST_INDEX(bytes_left);

			//插入到對應鏈表的頭部,進行回收利用
			((obj*)start_free)->free_list_link = *my_free_list;
			*my_free_list = (obj*)start_free;
		}

		//向操作系統索取新的內存塊
		start_free = (char*)malloc(bytes_to_get);

		//如果說,此時操作系統內存耗盡,無法成功分配 bytes_to_get 大小的內存,
		//向比 size 大的,最近的鏈表上索取一個 block ,進行切割
		if (0 == start_free)
		{
			obj *p;
			for (i = size; i <= __MAX_BYTES; i += __ALIGN)
			{
				obj **my_free_list = free_list + FREELIST_INDEX(i);

				p = *my_free_list;

				if (p != nullptr)
				{
					//相當於切了一塊 block 出去了
					*my_free_list = p->free_list_link;

					//把切出來的這一塊作爲 memory pool
					start_free = (char*)p;
					end_free = start_free + i;

					//再一次調用 chunk_alloc,這個時候,一定可以調用成功,分出一個 size 大小的 block
					return(chunk_alloc(size, nobjs));
				}
			}
			
			//右側的鏈表也沒有空閒 block 可用,此時調用第一種內存分配方式
			end_free = 0;
			start_free = (char*)malloc_allocate(bytes_to_get);
		}

		heap_size += bytes_to_get;
		end_free = start_free + bytes_to_get;

		//遞歸調用一次, 進入第一個 if 判斷條件內
		return(chunk_alloc(size, nobjs));
	}
}
  • 這個函數裏面,包含了向操作系統索取新的內存塊,memory pool 的處理,內存碎片的處理,當系統內存耗盡,無法獲取新的內存塊時的處理這樣幾個重要的功能,可以結合上一篇 的運行模式分析,比較好懂。

源碼地址

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