介紹:
設計記憶體池的目標是為了保證伺服器長時間高效的運行,通過對申請空間小而申請頻繁的對象進行有效管理,減少記憶體片段的產生,合理分配系統管理使用者記憶體,從而減少系統中出現有效空間足夠,而無法分配大塊連續記憶體的情況。
目標:
此次設計記憶體池的基本目標,需要滿足執行緒安全性(多線程),適量的記憶體泄露越界檢查,運行效率不太低於malloc/free方式,實現對4-128位元組範圍內的記憶體空間申請的記憶體池管理(非單一固定大小對象管理的記憶體池)。
記憶體池技術設計與實現
本記憶體池的設計方法主要參考SGI的alloc的設計方案,為了適合一般的應用,並在alloc的基礎上做一些簡單的修改。
Mempool的記憶體池設計方案如下(也可參考候捷《深入剖析STL》)
從系統申請大塊heap記憶體,在此記憶體上劃分不同大小的區塊,並把具有相同大小的區塊串連起來,組成一個鏈表。比如A大小的塊,組成鏈表L,當申請A大小時,直接從鏈表L頭部(如果不為空白)上取到一塊交給申請者,當釋放A大小的塊時,直接掛接到L的頭部。記憶體池的原理比較簡單,但是在具體實現過程中大量的細節需要注意。
1:位元組對齊。
為了方便記憶體池中對象的管理,需要對申請記憶體空間的進行調整,在Mempool中,位元組對齊的大小為最接近8倍數的位元組數。比如,使用者申請5個位元組,Mempool首先會把它調整為8位元組。比如申請22位元組,會調整為24,對比關係如下
序號 |
對齊位元組 |
範圍 |
0 |
8 |
1-8 |
1 |
16 |
9-16 |
2 |
24 |
17-24 |
3 |
32 |
25-32 |
4 |
40 |
33-40 |
5 |
48 |
41-48 |
6 |
56 |
49-56 |
7 |
64 |
57-64 |
8 |
72 |
65-72 |
9 |
80 |
73-80 |
10 |
88 |
81-88 |
11 |
96 |
89-96 |
12 |
104 |
97-104 |
13 |
112 |
105-112 |
14 |
120 |
113-120 |
15 |
128 |
121-128 |
(圖1)
對於超過128位元組的申請,直接調用malloc函數申請記憶體空間。這裡設計的記憶體池並不是對所有的對象進行記憶體管理,只是對申請記憶體空間小,而申請頻繁的對象進行管理,對於超過128位元組的對象申請,不予考慮。這個需要與實際項目結合,並不是固定不變的。實現對齊操作的函數如下
static size_t round_up(size_t size)
{
return (((size)+7) &~ 7);// 按8位元組對齊
}
2:構建索引表
記憶體池中管理的對象都是固定大小,現在要管理0-128位元組的範圍內的對象申請空間,除了採用上面提到的位元組對齊外,還需要變通一下,這就是建立索引表,做法如下;
static _obj* free_list[16];
建立一個包含16個_obj*指標的數組,關於_obj結構後面詳細講解。free_list[0]記錄所有空閑空間為8位元組的鏈表的首地址;free_list[1]對應16位元組的鏈表,free_list[2]對應24位元組的列表。free_list中的下標和位元組鏈表對應關係參考圖1中的“序號”和“對齊位元組”之間的關係。這種關係,我們很容易用演算法計算出來。如下
static size_t freelist_index(size_t size)
{
return (((size)+7)/7-1);// 按8位元組對齊
}
所以,這樣當使用者申請空間A時,我們只是通過上面簡單的轉換,就可以跳轉到包含A位元組大小的空閑鏈表上,如下;
_obj** p = free_list[freelist_index(A)];
3:構建空閑鏈表
通過索引表,我們知道mempool中維持著16條空閑鏈表,這些空閑鏈表中管理的空閑對象大小分別為8,16,24,32,40…128。這些空閑鏈錶鏈接起來的方式完全相同。一般情況下我們構建單鏈表時需要建立如下的一個結構體。
struct Obj
{
Obj *next;
Char* p;
Int iSize;
}
next指標指向下一個這樣的結構,p指向真正可用空間,iSize用於只是可用空間的大小,在其他的一些記憶體池實現中,還有更複雜的結構體,比如還包括記錄此結構體的上級結構體的指標,結構體中當前使用空間的變數等,當使用者申請空間時,把此結構體添加的使用者申請空間中去,比如使用者申請12位元組的空間,可以這樣做
Obj *p = (Obj*)malloc(12+sizeof(Obj));
p->next = NULL;
p->p = (char*)p+sizeof(Obj);
p->iSize = 12;
但是,我們並沒有採用這種方式,這種方式的一個缺點就是,使用者申請小空間時,記憶體池加料太多了。比如使用者申請12位元組時,而真實情況是記憶體池向記憶體申請了12+ sizeof(Obj)=12+12=24位元組的記憶體空間,這樣浪費大量記憶體用在標記記憶體空間上去,並且也沒有體現索引表的優勢。Mempool採用的是union方式
union Obj
{
Obj *next;
char client_data[1];
}
這裡除了把上面的struct修改為union,並把int iSize去掉,同時把char*p,修改為char client_data[1],並沒有做太多的修改。而優勢也恰恰體現在這裡。如果採用struct方式,我們需要維護兩條鏈表,一條鏈表是,已指派記憶體空間鏈表,另一條是未分配(空閑)空間鏈表。而我們使用索引表和union結構體,只需要維護一條鏈表,即未配置的空間鏈表。具體如下
索引表的作用有兩條1:如上所說,維護16條空閑鏈表2:變相記錄每條鏈表上空間的大小,比如下標為3的索引表內維持著是大小為24位元組的空閑鏈表。這樣我們通過索引表減少在結構體內記錄p所指向空間大小的iSize變數。從而減少4個位元組。
Union的特性是,結構內的變數是互斥存在的。再運行狀態下,只是存在一種變數類型。所以在這裡sizeof(Obj)的大小為4,難道這裡我們也需要把這4位元組也加到使用者申請空間中去嘛?其實不是,如果這樣,我們又抹殺了union的特性。
當我們構建空閑分配鏈表時,我們通過next指向下一個union結構體,這樣我們不使用p指標。當把這個結構體分配出去時,我們直接返回client_data的地址,此時client_data正好指向申請空間的首位元組。所以這樣,我們就不用在使用者申請空間上添加任何東西。
圖2
Obj的串連方式如上所示,這樣我們無需為使用者申請空間添加任何內容。
4:記錄申請空間位元組數
如果採用物件導向方式,或者我們在釋放記憶體池的空間時能夠明確知道釋放空間的大小,無需採用這種方式。
圖3
在C語言中的free沒有傳遞釋放空間大小,而可以正確釋放,在這裡也是模仿這種方式,採用這種記錄申請空間大小的方式去釋放記憶體。使用者申請空間+1操作將在位元組對齊之前執行,找到合適空間後,把首位元組改寫為申請空間的大小,當然1個位元組最多紀錄256個數,如果項目需要,可以設定為short類型或者int類型,不過這樣就需要佔用使用者比較大的空間。當釋放記憶體空間時,首先讀取這個位元組,擷取空間大小,進行釋放。為了便於對大於128位元組對象的大小進行合適的釋放,同時也對大於128位元組的記憶體申請,添加1位元組記錄大小。所以現在這裡限制了使用者記憶體申請空間不得大於255位元組,不過現在已經滿足項目要求。當然也可以修改為用short類型記錄申請空間的大小。
// 申請
*(( unsigned char *)result) = (size_t)n;
unsigned char * pTemp = (unsigned char*)result;
++pTemp;
result = (_obj*)pTemp;
return result;
// 釋放
unsigned char * pTemp = (unsigned char *)ptr;
--pTemp;
ptr = (void*)pTemp;
n = (size_t)(*( unsigned char *)ptr);
5:記憶體池的分配原理
在記憶體池的設計中,有兩個重要的操作過程1:chunk_alloc,申請大塊記憶體,2:refill回填操作,記憶體池初始化化時並不是為索引表中的每一項都建立空閑分配鏈表,這個過程會延遲到,只有使用者提取請求時才會建立這樣的分配鏈表。詳細參考如下代碼(在sgi中stl_alloc.h檔案中你也可以看到這兩個函數),主要步驟在注釋中已經說明。
/**
* @bri: 申請大塊記憶體,並返回size*(*nobjs)大小的記憶體塊
* @param: size,round_up對齊後的大小,nobjs
* @return: 返回指向第一個對象記憶體指標
*/
static char* 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 = (int)(__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);
/**< 把記憶體中剩餘的空間添加到freelist中 */
if(__bytes_left > 0)
{
_obj *VOLATILE* __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);
/*=======================================================================*/
memset(_start_free,0,__bytes_to_get);
/*=======================================================================*/
// 系統記憶體已經無可用記憶體,那麼從記憶體池中壓縮記憶體
if(0 == _start_free)
{
size_t __i;
_obj *VOLATILE* __my_free_list;
_obj *__p;
/**< 從freelist中逐項檢查可用空間(此時只收集比size對象大的記憶體空間) */
for (__i = size; __i <= (size_t)__MAX_BYTES; __i += __ALIGN)
{
__my_free_list = _free_list + freelist_index(__i);
__p = *__my_free_list;
/**< 找到空閑塊 */
if (__p != 0)
{
*__my_free_list = __p->free_list_link;
_start_free = (char*)__p;
_end_free = _start_free + __i;
return (chunk_alloc(size,nobjs));
}
}
_end_free = 0;
/**< 再次申請記憶體,可能觸發一個異常 */
_start_free = (char*)malloc(__bytes_to_get);
}
/**< 記錄當前記憶體池的容量 */
_heap_size += __bytes_to_get;
_end_free = _start_free + __bytes_to_get;
return (chunk_alloc(size,nobjs));
}
}
/*=======================================================================*/
/**
* @bri: 填充freelist的串連,預設填充20個
* @param: __n,填充對象的大小,8位元組對齊後的value
* @return: 空閑
*/
static void* refill(size_t n)
{
int __nobjs = 20;
char* __chunk = (char*)chunk_alloc(n, &__nobjs);
_obj *VOLATILE* __my_free_list;
_obj *VOLATILE* __my_free_list1;
_obj * __result;
_obj * __current_obj;
_obj * __next_obj;
int __i;
// 如果記憶體池中僅有一個對象
if (1 == __nobjs)
return(__chunk);
__my_free_list = _free_list + freelist_index(n);
/* Build free list in chunk */
__result = (_obj*)__chunk;
*__my_free_list = __next_obj = (_obj*)(__chunk + n);
__my_free_list1 = _free_list + freelist_index(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);
}
經過上面操作後,記憶體池可能會成為如下的一種狀態。從圖上我們可以看到,已經構建了8,24,88,128位元組的空閑分配鏈表,而其他沒有分配空閑分配鏈表的他們的指標都指向NULL。我們通過判斷索引表中的指標是否為NULL,知道是否已經構建空閑分配表或者空閑分配表是否用完,如果此處指標為NULL,我們調用refill函數,重新申請20個這樣大小的記憶體空間,並把他們串連起來。在refill函數內,我們要查看大記憶體中是否有可用記憶體,如果有,並且大小合適,就返回給refill函數。
圖4
6:安全執行緒
採用互斥體,保證安全執行緒。
記憶體池測試
記憶體池的測試主要分兩部分測試1:單線程下malloc與mempool的分配速度對比2:多線程下malloc和mempool的分配速度對比,我們分為4,10,16個線程進行測試了。
測試環境:作業系統:windows2003+sp1,VC7.1+sp1,硬體環境:intel(R) Celeron(R) CPU 2.53GHz,512M實體記憶體。
申請記憶體空間設定如下
#define ALLOCNUMBER0 4
#define ALLOCNUMBER1 7
#define ALLOCNUMBER2 23
#define ALLOCNUMBER3 56
#define ALLOCNUMBER4 10
#define ALLOCNUMBER5 60
#define ALLOCNUMBER6 5
#define ALLOCNUMBER7 80
#define ALLOCNUMBER8 9
#define ALLOCNUMBER9 100
Malloc方式和mempool方式均使用如上資料進行記憶體空間的申請和釋放。申請過程,每次迴圈申請釋放上述資料20次
我們對malloc和mempool,分別進行了如下申請次數的測試(單位為萬)
2 |
10 |
20 |
30 |
40 |
50 |
80 |
100 |
150 |
200 |
malloc和mempool在單線程,多線程,release,debug版的各種測試資料,形成如下的統計圖
圖5
可以看到mempool無論在多線程還是在單線程情況下,mempool的速度都優於malloc方式的直接分配。
Malloc方式debug模式下,在不同的線程下,已耗用時間如下,通過圖片可知,malloc方式,在debug模式下,申請空間的速度和多線程的關係不大。多線程方式,要略快於單線程的運行實現。
圖6
Malloc方式release模式測試結果如下。
圖7
多線程的優勢,逐漸體現出來。當執行200w次申請和釋放時,多線程要比單線程快1500ms左右,而4,10,16個線程之間的差別並不是特別大。不過整體感覺4個線程的已耗用時間要稍微高於10,16個線程的情況下,意味著進程中線程越多用線上程切換上的時間就越多。
下面是mempool在debug測試結果
圖8
下面是mempool在release模式下的測試結果
圖9
以上所有統計圖中所用到的資料,是我們測試三次後平均值。
通過上面的測試,可以知道mempool的效能基本上超過直接malloc方式,在200w次申請和釋放的情況下,單線程release版情況下,mempool比直接malloc快110倍。而在4個線程情況下,mempool要比直接malloc快7倍左右。以上測試只是申請速度的測試,在不同的壓力情況下,測試結果可能會不同,測試結果也不能說明mempool方式比malloc方式穩定。
小結:記憶體池基本上滿足初期設計目標,但是她並不是完美的,有缺陷,比如,不能申請大於256位元組的記憶體空間,無記憶體越界檢查,無記憶體自動回縮功能等。只是這些對我們的影響還不是那麼重要。
由於這是一個公司項目,代碼涉及著作權,所以不能發布出來。如果你想做自己的記憶體池,可以與我聯絡ugg_xchj#hotmail.com.