1. 程式人生 > >STL原始碼分析之第二級配置器

STL原始碼分析之第二級配置器

前言

第一級是直接呼叫malloc分配空間, 呼叫free釋放空間, 第二級三就是建立一個記憶體池, 小於128位元組的申請都直接在記憶體池申請, 不直接呼叫mallocfree. 本節分析第二級空間配置器, STL將第二級配置器設定為預設的配置器, 所以只要一次申請的空間不超過128位元組就預設在記憶體池中申請空間, 超過才會呼叫第一級配置器.

第二級配置器

首先先來介紹3個常量.

  1. __ALIGN : 以8位元組進行對齊
  2. __MAX_BYTES : 二級分配器最大分配的記憶體大小
  3. __NFREELISTS : 128位元組能分配的的連結串列個數, 並且從每個連結串列儲存的記憶體大小都是8的倍數, 而且都比前一個大8位元組, 也就是分別是8, 16, 32…128位元組
// 二級配置器
enum {__ALIGN = 8}; // 設定對齊要求. 對齊為8位元組, 沒有8位元組自動補齊
enum {__MAX_BYTES = 128};   // 第二級配置器的最大一次性申請大小, 大於128就直接呼叫第一級配置器
enum {__NFREELISTS = __MAX_BYTES/__ALIGN};  // 連結串列個數, 分別代表8, 16, 32....位元組的連結串列

再介紹一個巨集操作, 這是進行對齊操作, 將不滿8的倍數的填充成8的倍數.

static size_t FREELIST_INDEX(size_t bytes) \
{\
    return
(((bytes) + ALIGN-1) / __ALIGN - 1);\ }

從allocate先切入分析

  1. 先判斷申請的位元組大小是不是大於128位元組, 是, 則交給第一級配置器來處理. 否, 繼續往下執行
  2. 找到分配的地址對齊後分配的是第幾個大小的連結串列.
  3. 獲得該連結串列指向的首地址, 如果連結串列沒有多餘的記憶體, 就先填充連結串列.
  4. 返回連結串列的首地址, 和一塊能容納一個物件的記憶體, 並更新連結串列的首地址
static void * allocate(size_t n)
{
      obj * __VOLATILE * my_free_list;
obj * __RESTRICT result; if (n > (size_t) __MAX_BYTES) { 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)); return r; } *my_free_list = result -> free_list_link; return (result); };

refill記憶體填充.

  1. 向記憶體池申請空間的起始地址
  2. 如果只申請到一個物件的大小, 就直接返回一個記憶體的大小, 如果有更多的記憶體, 就繼續執行
  3. 從第二個塊記憶體開始, 把從記憶體池裡面分配的記憶體用連結串列給串起來, 並返回一個塊記憶體的地址給使用者
// 記憶體填充
template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n)
{
  	int nobjs = 20;
  	char * chunk = chunk_alloc(n, nobjs);             // 向記憶體池申請空間的起始地址
  	obj * __VOLATILE * my_free_list;
  	obj * result;
  	obj * current_obj, * next_obj;
  	int i;

  	// 如果只申請到一個物件的大小, 就直接返回一個記憶體的大小
  	if (1 == nobjs) return(chunk);
  	my_free_list = free_list + FREELIST_INDEX(n);

  	// 申請的大小不只一個物件的大小的時候
  	result = (obj *)chunk;
  	// my_free_list指向記憶體池返回的地址的下一個對齊後的地址
  	*my_free_list = next_obj = (obj *)(chunk + n);
  	// 這裡從第二個開始的原因主要是第一塊地址返回給了使用者, 現在需要把從記憶體池裡面分配的記憶體用連結串列給串起來
  	for (i = 1; ; i++) 
  	{
	    current_obj = next_obj;
    	next_obj = (obj *)((char *)next_obj + n);
	    if (nobjs - 1 == i) 
        {
			current_obj -> free_list_link = 0;
      		break;
    	} 
        else 
        {
      		current_obj -> free_list_link = next_obj;
    	}
  		}
  	return(result);
}

再從deallocate結束

  1. 釋放的記憶體大於128位元組直接呼叫一級配置器進行釋放
  2. 將記憶體直接還給對應大小的連結串列就行了, 並不用直接釋放記憶體, 以便後面分配記憶體的時候快速.
static void deallocate(void *p, size_t n)
{
      obj *q = (obj *)p;
      obj * __VOLATILE * my_free_list;
	
      // 釋放的記憶體大於128位元組直接呼叫一級配置器進行釋放
      if (n > (size_t) __MAX_BYTES) 
      {
        malloc_alloc::deallocate(p, n);
        return;
      }
      my_free_list = free_list + FREELIST_INDEX(n);
      q -> free_list_link = *my_free_list;
      *my_free_list = q;
}

統一的介面

定義符合STL規格的配置器介面, 不管是一級配置器還是二級配置器都是使用這個介面進行分配的. 預設設定為第二級配置器

// 定義符合STL規格的配置器介面, 不管是一級配置器還是二級配置器都是使用這個介面進行分配的
template<class T, class Alloc>
class simple_alloc {
  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和free效率是很低的, 特別是很小記憶體的時候.

STL預設的就是第二級配置器, 它會自動判斷我們使用哪一個配置器.