寫服務器的,通常會涉及到內存池的東西,自己在這方面也看了寫了一些東西,有些體會,寫出來跟大家分享下。
內存池基本包含以下幾個東西,第一,初始化。第二,分配內存。第三,回收內存。所謂初始化,就是在服務器啟動的時候,或者第一次需要內存的時候,系統分配很大的一塊內存,方便之后的使用。分配內存,就是從內存池中取出需要的內存給外部使用,當然這里需要考慮的是當內存池中沒有內存可分配時候的處理。回收內存,簡單來說,就是外面對象生命期結束了,將分配出去的內存回收入內存池中。好了簡單概念就說完了,我們先來看一種最簡單的設計方式。
//為了方便描述,這里附上幾個簡單的鏈表操作宏
#define INSERT_TO_LIST( head, item, prev, next ) \
do{ \
if ( head ) \
(head)->prev = (item); \
(item)->next = (head); \
(head) = (item); \
}while(0)
#define REMOVE_FROM_LIST(head, item, prev, next) \
do{ \
if ( (head) == (item) ) \
{ \
(head) = (item)->next; \
if ( head ) \
(head)->prev = NULL; \
} \
else \
{ \
if ( (item)->prev ) \
(item)->prev->next = (item)->next; \
\
if ( (item)->next ) \
(item)->next->prev = (item)->prev; \
} \
}while(0)
struct student
{
char name[32];
byte sex;
struct student *prev,*next;
};
static struct mem_pool
{
//該指針用來記錄空閑節點
struct student *free;
//該變量記錄分配結點個數
size_t alloc_cnt;
}s_mem_pool;
//分配內存“塊”的函數
bool mem_pool_resize(size_t size)
{
//該函數創建size個不連續的對象,把他們通過鏈表的方式加入到s_mem_pool.free中
for ( size_t i = 0;i < size;++i )
{
struct student *p = (struct student *)malloc(sizeof(struct student));
if ( !p )
return false;
p->prev = p->next = NULL;
INSERT_TO_LIST(s_mem_pool.free,p,prev,next);
}
s_mem_pool.alloc_cnt += size;
}
#define MEM_INIT_SIZE 512
#define MEM_INC_SIZE 256
//初始化函數
bool mem_pool_init()
{
if ( !mem_pool_resize(MEM_INIT_SIZE) )
return false;
return true;
}
struct student *get_data()
{
if ( s_mem_pool.free == NULL )
{
if ( !mem_pool_resize(MEM_INC_SIZE) )
return NULL;
}
struct student *ret = s_mem_pool.free;
REMOVE_FROM_LIST(s_mem_pool.free,ret,prev,next)
return ret;
}
void free_data(struct student *p)
{
if ( !p )
return;
memset(p,0,sizeof(struct student));
INSERT_TO_LIST(s_mem_pool.free,p,prev,next)
}
好了最簡單的內存池的大致框架就是這樣。我們先來看下他的過程。首先,在mem_pool_init()函數中,他先分配512個不連續的student對象。每分配出來一個就把它加入到free鏈表中,初始化完成后內存池大概是這樣的

接下來就是從內存池中取出一個對象get_data()。函數先去判斷是否有空閑的對象,有則直接分配,否則再向系統獲取一"塊"大的內存。調用一次后的內存池大概是這樣的

釋放對象,再把對象加入到Free鏈表中。
以上就是過程的簡單分析,下面我們來看看他的缺點。
第一,內存不是連續的,容易產生碎片
第二,一個類型就得寫一個這樣的內存池,很麻煩
第三,為了構建這個內存池,每個沒對象必須加上一個prev,next指針
好了,我們來優化一下它。我們重新定義下我們的結構體
union student
{
int index;
struct
{
char name[32];
byte sex;
}s;
};
static struct mem_pool
{
//該下標用來記錄空閑節點
int free;
//內存池
union student *mem;
//已分配結點個數
size_t alloc_cnt;
}s_mem_pool;
//分配內存塊的函數
bool mem_pool_resize(size_t size)
{
size_t new_size = s_mem_pool.alloc_cnt+size;
union student *tmp = (union student *)realloc(s_mem_pool.mem,new_size*sizeof(union student));
if ( !tmp )
return false;
memset(tmp+s_mem_pool.alloc_cnt,0,size*sizeof(union student));
size_t i = s_mem_pool.alloc_cnt;
for ( ;i < new_size - 1;++i )
{
tmp[i].index = i + 1;
}
tmp[i].index = -1;
s_mem_pool.free = s_mem_pool.alloc_cnt;
s_mem_pool.mem = tmp;
s_mem_pool.alloc_cnt = new_size;
return true;
}
#define MEM_INIT_SIZE 512
#define MEM_INC_SIZE 256
//初始化函數
bool mem_pool_init()
{
if ( !mem_pool_resize(MEM_INIT_SIZE) )
return false;
return true;
}
union student *get_data()
{
if ( s_mem_pool.free == -1 )
{
if ( !mem_pool_resize(MEM_INC_SIZE) )
return NULL;
}
union student *ret = s_mem_pool.mem+s_mem_pool.free;
s_mem_pool.free = ret->index;
return ret;
}
void free_data(union student *p)
{
if ( !p )
return;
p->index = s_mem_pool.free;
s_mem_pool.free = p - s_mem_pool.mem;
}
我們來看看改進了些什么。第一student改成了聯合體,這主要是為了不占用額外的內存,也就是我們上面所說的第三個缺點,第二,我們使用了realloc函數,這樣我們可以使我們分配出來的內存是連續的。我們初始化的時候多了一個for循環,這是為了記錄空閑對象的下標,當我們取出一個對象時,free可以立刻知道下一個空閑對象的位置,釋放的時候,對象先記錄free此時的值,接著再把free賦值成該對象在數組的下標,這樣就完成了回收工作。
我們繼續分析這段代碼,問題在realloc函數上,如果我們的s_mem_pool.mem已經很大了,在realloc的時候我們都知道,先要把原來的數據做一次拷貝,所以如果數據量很大的情況下做一次拷貝,是會消耗性能的。那這里有沒有好的辦法呢,我們進一步優化
思路大概是這樣
初始化

再次分配的時候,我們只需要重新分配新的內存單元,而不需要拷貝之前的內存單元。

因此基于此思路,我們修改我們的代碼
#include <stdio.h>
#include <stdlib.h>
struct student
{
int index;
char name[32];
byte sex;
};
static struct mem_pool
{
//該下標用來記錄空閑節點
int free;
//內存池
struct student **mem;
//已分配塊個數
size_t block_cnt;
}s_mem_pool;
#define BLOCK_SIZE 256 //每塊的大小
//分配內存塊的函數
bool mem_pool_resize(size_t block_size)
{
size_t new_cnt = s_mem_pool.block_cnt + block_size;
struct student **tmp = (struct student **)realloc(s_mem_pool.mem,new_size*sizeof(struct student *));
if ( !tmp )
return false;
memset(tmp+s_mem_pool.block_cnt,0,size*sizeof(struct student*));
for ( size_t i = s_mem_pool.block_cnt;i < new_cnt;++i )
{
tmp[i] = (struct student *)calloc(BLOCK_SIZE,sizeof(struct student));
if ( !tmp[i] )
return false;
size_t j = 0;
for(;j < BLOCK_SIZE - 1;++j )
{
tmp[i][j].index = i*BLOCK_SIZE+j+1;
}
if ( i != new_cnt-1 )
tmp[i][j].index = (i+1)*BLOCK_SIZE;
else
tmp[i][j].index = -1;
}
s_mem_pool.free = s_mem_pool.alloc_cnt*BLOCK_SIZE;
s_mem_pool.mem = tmp;
s_mem_pool.block_cnt = new_cnt;
return true;
}
#define MEM_INC_SIZE 10
//初始化函數
bool mem_pool_init()
{
if ( !mem_pool_resize(MEM_INIT_SIZE) )
return false;
return true;
}
struct student *get_data()
{
if ( s_mem_pool.free == -1 )
{
if ( !mem_pool_resize(MEM_INC_SIZE) )
return NULL;
}
struct student *ret = s_mem_pool.mem[s_mem_pool.free/BLOCK_SIZE]+s_mem_pool.free%BLOCK_SIZE;
int pos = s_mem_pool.free;
s_mem_pool.free = ret->index;
ret->index = pos;
return ret;
}
void free_data(struct student *p)
{
if ( !p )
return;
int pos = p->index;
p->index = s_mem_pool.free;
s_mem_pool.free = pos;
}
這里不一樣的地方主要在mem_pool_resize函數中,mem變成了2級指針,每次realloc的時候只需要分配指針數組的大小,無須拷貝對象,這樣可以提高效率,但是為了在釋放的時候把對象放回該放的位置,我們這里在結構體里加入了index變量,記錄它的下標。在內存池里,它表示下個空閑對象的下標,在內存池外,它表示在內存池中的下標。總的來說滿足了一個需求,卻又帶來了新的問題,有沒有更好的方法呢,答案是肯定,不過今天先寫到這里,明天繼續。
posted on 2012-07-19 11:41
梨樹陽光 閱讀(3520)
評論(2) 編輯 收藏 引用 所屬分類:
C