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 空間配置器的記憶體分配機制。