1. 程式人生 > >STL原始碼——SGI 空間配置器

STL原始碼——SGI 空間配置器

本文主要參考STL原始碼剖析,但書中對某些地方寫的不是很詳細,所以根據個人的理解增加了一些細節的說明,便於回顧。

由於小型區塊分配時可能造成記憶體破碎問題,SGI設計了兩級配置器,第一級配置器直接使用malloc和free,第二級配置器則視情況採取不同的策略:當配置的區塊超過128Bytes時,呼叫第一級配置器;當配置區塊小於128Bytes時,採用複雜的記憶體池整理方式,而不再求助於第一級配置器。使用第一級配置器還是同時開放第二級配置器,取決於__USE_MALLOC是否被定義。

#ifdef __USE_MALLOC
...
typedef __malloc_alloc_template<0> malloc_alloc;
typedef malloc_alloc alloc;   //令alloc為第一級配置器
#else
...
//令alloc為第二級配置器
typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS,0> alloc;
#endif

其中__malloc_alloc就是第一級配置器,__default_alloc_template就是第二級配置器

無論alloc被定義為何種配置器,SGI再為之包裝一個介面如下,使配置器的介面能符合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) Allocate::deallocate(p, n * sizeof(T));
	}
	static void deallocate(T *p){
		Alloc::deallocate(p, sizeof(T));
	}
}

可以看出,其內部四個成員函式都是單純的函式呼叫。SGI STL容器全都使用這個simple_alloc介面(預設使用alloc為配置器)。

一二級配置器的關係如下(圖摘自STL原始碼剖析)



介面包裝及實際運用方式如下(圖摘自STL原始碼剖析):


第二級配置器的設計思想是:每次配置一大塊連續記憶體,並維護其對應的自由連結串列(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 位元組的小額區塊。每個free-lists是一系列大小相同的區塊串成的連結串列,便於分配和回收。free-lists的節點結構如下:

union obj{
	union obj* free_list_link;
	char client_data[1]   /* the client sees this */
}

插曲:書上對節點如此設計的原因解釋如下:不造成記憶體的浪費(儲存額外的連結串列指標)。

STL原始碼中使用聯合union來設計,並且第二個欄位設定為client_data[1],是使用了柔性陣列。從第一個欄位看,obj可被視為一個指標,指向另一個obj,從第二個地段看,obj可被視為一個大小不定的記憶體區塊(柔性陣列),陣列長度視分配的記憶體而定。

柔性陣列簡單介紹如下:

結構中最後一個元素允許是未知大小的陣列(長度為0或者1),這個陣列就是柔性陣列。但結構中的柔性陣列前面必須至少一個其他成員,柔性陣列不佔用結構體的記憶體。包含柔陣列成員的結構用malloc函式進行記憶體的動態分配,且分配的記憶體應該大於結構的大小以適應柔性陣列的預期大小,如下一個例子:

Struct Packet
{
int len;
char data[1]; //使用[1]比使用[0]相容性好
};

對於編譯器而言,陣列名僅僅是一個符號,它不會佔用任何空間,它在結構體中,只是代表了一個偏移量。當使用packet儲存資料時,使用

char *tmp = (char*)malloc(sizeof(Packet)+1024) 

申請一塊連續的記憶體空間,這塊記憶體空間的長度是Packet的大小加上1024資料的大小。包中的資料存放在data中。

回到正題,這裡用柔性陣列,主要是用來表示16種不同大小的記憶體區塊(前面提到過的,8,16,24……),在原始碼中根本沒有用到client_data,而obj是在記憶體配置器內部定義的,使用者更是用不上。或許這就是設計者對程式碼精煉的追求吧。使用union聯合體的記憶體使用方式如下:(union大小為4)


所以使用起來正如書中那樣:


第二級配置器部分實現內容如下:

enum {__ALIGN = 8};  //小型區塊的上調邊界
enum {__MAX_BYTES = 128};   //小型區塊的上界
enum {__NFREELISTS = __MAX_BYTES/__ALIGN};   //free-list個數

template <bool threads, int inst>
class __default_alloc_template {

private:
	/*將bytes上調至8的倍數
	用二進位制理解,byte整除align時尾部為0,結果仍為byte;否則尾部肯定有1存在,加上
	align - 1之後定會導致第i位(2^i = align)的進位,再進行&操作即可得到8的倍數
	*/
	static size_t ROUND_UP(size_t bytes) {
        return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1));
  	}
private:
	union obj {   //free-list的節點
	    union obj * free_list_link;
	    char client_data[1];    /* The client sees this.     */
	};

private:
	//16個free-lists
	static obj * __VOLATILE free_list[__NFREELISTS]; 
	//根據區塊大小,找到合適的free-list,返回其下標(從0起算)
  	static  size_t FREELIST_INDEX(size_t bytes) {
        return (((bytes) + __ALIGN-1)/__ALIGN - 1);
  }
  //返回一個大小為n的物件,並可能編入大小為n的區塊到相應的free-list
  static void *refill(size_t n);
  //配置一大塊空間,可容納nobjs個大小為“size”的區塊
  //如果配置nobjs個區塊有所不便,nobjs可能會降低
  static char *chunk_alloc(size_t size, int &nobjs);

  //Chunk allocation state
  static char *start_free;
  static char *end_free;
  static size_t heap_size;

public:
	static void * allocate(size_t n);
	static void * deallocatr(void *p, size_t n);
	static void * reallocate(void *p, size_t old_sz, size_t new_sz);
};

//以下是static data member的定義與初值設定
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 * volatile
__default_alloc_template<threads, inst>::free_list[__NFREELISTS] = 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, };

弄清結點結構之後,先說一下allocate的基本流程,有了大概的瞭解之後,再進入原始碼分析。allocate首先判斷所需區塊的大小,大於128Bytes就呼叫第一級配置器,小於128Bytes就檢查對應的free-list,如果free-list之內有可用的區塊,就直接拿來用,否則就將區塊大小調至8的倍數,呼叫refill函式為free-list重新填充空間。

allocate函式如下:

//n must be > 0
static void * allocate(size_t n)
{
    obj * __VOLATILE * my_free_list;
    obj * __RESTRICT result;

    //大於128就呼叫第一級配置器
    if (n > (size_t) __MAX_BYTES) {
        return(malloc_alloc::allocate(n));
    }
    //尋找16個free-lists中適當的一個
    my_free_list = free_list + FREELIST_INDEX(n);
    result = *my_free_list;
    if (result == 0) {
    	//沒找到可用的free-list,準備重新填充free-list
        void *r = refill(ROUND_UP(n));
        return r;
    }
    //調整free-list,指向撥出區塊的下一個區塊
    *my_free_list = result -> free_list_link;
    return (result);
};

refill呼叫chunk_alloc獲取連續的記憶體空間,然後將這塊連續的記憶體空間編排入相應的free-list中(預設情況下取得20個區塊,若記憶體池空間不夠,獲得區塊數可能小於20),最後返回這塊記憶體空間的首址。而chunk_alloc負責從記憶體池中取空間給free-list使用,由於只有這裡涉及到了記憶體池容量的變化,故記憶體池的起始、結束位置只在chunk_alloc中發生變化。

refill函式如下:

//返回一個大小為n的物件,並且有時候會適當的free-list增加節點
//假設n已經適當上調至8的倍數
template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n)
{
    int nobjs = 20;
    //嘗試獲得nobjs個區塊作為free-list的新節點
    char * chunk = chunk_alloc(n, nobjs);
    obj * __VOLATILE * my_free_list;
    obj * result;
    obj * current_obj, * next_obj;
    int i;

    //如果只獲得一個區塊,這個區塊就分配給呼叫者使用,free-list無新增區塊
    if (1 == nobjs) return(chunk);
    //否則調整free-list 納入新節點
    my_free_list = free_list + FREELIST_INDEX(n);

    //在chunk這段連續記憶體內建立free-list
	result = (obj *)chunk;   //這一塊準備返回給客戶端
	//將free-list指向新配置的連續記憶體空間
	//allocate中my_free-list為0才進入本函式,故無需儲存現在的*my_free-list,直接覆蓋即可
	*my_free_list = next_obj = (obj *)(chunk + n);

	//將free-list的各節點串接起來     
	for (i = 1; ; i++) {
		current_obj = next_obj;
		next_obj = (obj *)((char *)next_obj + n);  //每一個區塊大小為n
		if (nobjs - 1 == i) {  //最後一塊
		    current_obj -> free_list_link = 0;
		    break;
		} else {
		    current_obj -> free_list_link = next_obj;
		}
	}
    return(result);
}

chunk_alloc取空間的原則如下:儘量從記憶體池中取,記憶體池不夠了,才使用free-list中的可用區塊。具體分三種情況:

①若當前記憶體池剩餘空間完全滿足需求,直接從記憶體池中撥出去,調整記憶體池起址即可;

②記憶體池剩餘空間不能完全滿足,但足以應對一個(含)以上的區塊,一個給客戶端使用,剩餘的編入free-list;③記憶體池連一個區塊的大小都無法提供,由於記憶體池分配時大小為8的倍數,每次撥出也是8的倍數,故剩餘空間也是8的倍數,可以編入一個區塊到相應大小的free-list中。此時記憶體池全部容量已用完。接下來使用heap分配新的記憶體(由於記憶體池中的記憶體要保持連續,否則按區塊大小編排free-list也無從談起,故在使用heap分配記憶體之前,記憶體池中的記憶體要保證全部用完)。

i.若堆空間也不足了,那麼從size起,在每一個free-list中尋找可用區塊,直到找到可用區塊,將該區塊歸還給記憶體池,再呼叫一次chunk_alloc(這次呼叫一定進入情況①或者②),從而修改調整記憶體池、nobjs。若free-lists中都沒有一個可用區塊,則呼叫第一級配置器,看out-of-memory機制是否有對策。

ii.否則,直接使用堆分配的記憶體,此時記憶體池已有足夠的空間,再呼叫一次chunk_alloc,調整nobjs。

chunk_alloc函式程式碼如下:

//size此時已適當上調至8的倍數
template <bool threads, int inst>
char*
__default_alloc_template<threads, inst>::chunk_alloc(size_t size, int& nobjs)
{
    char * result;
    size_t total_bytes = size * nobjs;   //8的倍數
    size_t bytes_left = end_free - start_free;  //8的倍數

    if (bytes_left >= total_bytes) {  //情況1
    	//記憶體池剩餘空間完全滿足需求量
        result = start_free;
        start_free += total_bytes;
        return(result);
    } else if (bytes_left >= size) {  //情況2
    	//雖不足以完全滿足,但足夠供應一個(含)以上的區塊
    	//從start_free開始一共total_bytes分配出去,其中前size個bytes給客戶端,剩餘的給free-list
        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) {
            obj * __VOLATILE * my_free_list =
                        free_list + FREELIST_INDEX(bytes_left); //找到大小相同區塊所在的free-list

            ((obj *)start_free) -> free_list_link = *my_free_list;  //將記憶體池剩餘空間編入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-lists中找尋可用的區塊(其大小夠用)
            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;
                    //再次從記憶體池中索要連續空間來滿足客戶端需求
                    return(chunk_alloc(size, nobjs));  //由於此時i >= size,故此次只會進入情況1/2
                }
            }
	    	end_free = 0;	//沒有可用區塊歸還到記憶體池,記憶體池仍為空
	    	//呼叫第一級配置器,看out-of-memory機制是否能改善
            start_free = (char *)malloc_alloc::allocate(bytes_to_get);
            
        }
        //記憶體池獲得新的連續空間
        heap_size += bytes_to_get;
        end_free = start_free + bytes_to_get;
        //再次嘗試分配
        return(chunk_alloc(size, nobjs));
    }
}

以上就是SGI 空間配置器的記憶體分配機制。