內存池實現與分析
描述
程序中不可避免的因為需要動態分配內存,而大量使用堆上的內存。如果使用系統默認的函數new/delete或malloc/free來分配和釋放堆上的內存,效率不高,同時還可能產生大量的內存碎片,導致長時間運行后性能愈發下降。為了提高性能,通常就需要考慮使用一些數據結構和算法來減少動態分配的發生,這也是內存池這個思想的來源。
在我們的服務器里,可以看到大量頻繁申請和銷魂內存的情況發生在接收處理網絡數據的部分里,所以在這一部分的處理中我們就需要考慮使用內存池來優化性能。
算法思想
首先使用的方法是類似 SGI STL 中的 allocator 內存分配器的實現方式。設計了一個數組,負責管理內存頁(MemPage)。每一個內存頁都可分配連固定大小,范圍在 8Bytes 到 64MBytes 之間的內存塊。
內存塊是在創建MemPage申請的一段連續大小的空間,同時另外用一個數組記錄每個塊所在的內存地址,分配的時候分配空閑的內存空間,釋放的時候通過修改數組指向的位置達到釋放的目的。
結構如下:
實現細節
首先定義的結構大小限制如下:
#define _MIN_BLOCK_SIZE_ (8) //單內存塊最小限制為8 Byte
#define _MAX_BLOCK_SIZE_ (1 << 26 ) //單內存塊最大限制為64 MByte
#define _PAGE_MIN_SIZE_ (1024 * 1024) //單頁大小最小限制為1 MByte
#define _PAGE_INDEX_COUNT_ (27) //對應上面的26位大小限制,所以數組定為27
#define _PAGE_COUNT_ (4096) //限制分配不超過4096頁
#define _PAGE_MIN_BITS_ (20) //單頁大小限制的位數,對應 _PAGE_MIN_SIZE_ 的大小
然后定義的分配器結構如下:
class CAllocator
{
public:
CAllocator();
virtual ~CAllocator();
virtual void *malloc (size_t nbytes);
virtual void free (void *ptr);
virtual void *realloc( void * pTemp , size_t nSize );
private:
size_t _lastIndex[_PAGE_INDEX_COUNT_]; //記錄每一位最后用來分配的索引
size_t _pageCount[_PAGE_INDEX_COUNT_]; //記錄每一位的已分配頁數量
CMemPage* _bitPages[_PAGE_INDEX_COUNT_][_PAGE_COUNT_]; //管理每一位的頁mempage
CMemPage* _pages[_PAGE_COUNT_]; //管理所有頁mempage
};
內存頁MemPage設計:
內存頁結構如下:
class CMemPage
{
private:
size_t _pageSize; //頁大小
size_t _blockSize; //塊大小
size_t _blockCount; //塊數量
size_t _freeIndex; //空閑索引
size_t* _freeBlocks; //塊地址數組
};
內存頁限制最小大小為1MByte,每次申請的大小都在該大小以上,分配的時候根據2進制位數計算出申請塊的大小,再根據該塊大小確定頁大小並向系統申請內存。如果單塊大小小於1MByte,則一個頁中存在多個塊。
頁中額外構建了一個數組,數組中每個元素均為頁中的塊對應的地址。
static MemPage* mallocPage(size_t blockSize)
{
size_t size = getBit(blockSize);
size_t pageSize = _PAGE_MIN_SIZE_ + sizeof(MemPage);
if(size > _PAGE_MIN_SIZE_)
{
pageSize = size + sizeof(MemPage);
}
void* buf = ::malloc(pageSize);
if(NULL == buf) return NULL;
MemPage page(size, pageSize, buf);
memcpy(buf, &page, sizeof(page));
return (MemPage*)buf;
}
MemPage(size_t blockSize, size_t pageSize, void* const start)
:_pageSize(0),_blockSize(0),_freeIndex(0)
{
memset(this, 0, sizeof(MemPage));
_pageSize = pageSize;
_blockSize = blockSize;
if(blockSize < _MIN_BLOCK_SIZE_)
{
_blockSize = _MIN_BLOCK_SIZE_;
}
_blockCount = (_pageSize - sizeof(MemPage)) / blockSize;
_freeBlocks = (size_t*)::malloc(_blockCount * sizeof(size_t));
for(size_t i = 0; i < _blockCount; ++i)
{
_freeNodes[i] = (size_t)((char*)start + sizeof(MemPage) + i * _blockSize);
}
_freeIndex = _blockCount;
}
分配操作:
當需要一段空間時,計算最接近的2次冪向上取整,算出位數,在下直接索引到該位數下的MemPage數組,然后尋找還有空閑位置的MemPage,分配一塊空間。
例如需要10Bytes,最接近的是16Bytes,索引到_usedPage[4],然后獲取上一次最后分配的索引和實際分配的頁數量:
//malloc函數
//計算出 bit = getBit(10) = 4,以下直接使用4來說明
lastIdx = _lastIndex[4];
pageCount = _pageCount[4];
//在lastIdx~pageCount之間找空余頁
for (int i = lastIdx; i < pageCount; ++i)
{
if (_bitPages[4][i]->isFree())
{
//分配內存
_lastIndex[4] = i;
return _bitPages[4][i]->malloc(bytes);
}
}
//如果在lastIdx~pageCount之間都無法找到空余頁,尋找0~lastIdx
for (int i = 0; i < lastIdx; ++i)
{
if (_bitPages[4][i]->isFree())
{
//分配內存
_lastIndex[4] = i;
return _bitPages[4][i]->malloc(bytes);
}
}
//如果依然無法分配,新建頁
MemPage *memPage = MemPage::mallocPage(bytes);
if( NULL != memPage )
{
_bitPages[4][ _pageCount[4]++ ] = memPage;
size_t index = (size_t)memPage >> _PAGE_MIN_BITS_;
_pages[ index ] = memPage;
return memPage->malloc(bytes);
}
上面計算index的方法是因為按照規定頁大小限制1MB,即內存地址間隔最小也在20位以上,可通過位移操作消除即可得到唯一索引,同時也是在釋放內存和重分配的時候直接通過地址計算出該內存塊所在內存頁。
其中,尋找到適合的頁后,在頁中取空閑塊的方法如下:
void* mallocNode(size_t size)
{
return (void*)_freeBlocks[--_freeIndex];
}
釋放操作:
在釋放內存的時候,就可以通過上面使用的計算index方法同樣計算出當前內存塊所在的index,從而找到對應的頁並釋放。釋放的時候只需要將頁中空閑內存索引+1並將空閑內存地址數組中重新記錄該塊地址即可。
void CAllocator::free(void *ptr)
{
if(!ptr)
{
return;
}
size_t index = (size_t)ptr >> (_PAGE_MIN_BITS_);
MemPage* memPage = _memPages[index];
if(!memPage || memPage > ptr)
{
--index;
}
memPage = _memPages[index];
memPage->freeNode(ptr);
}
bool freeNode(void* node)
{
_freeBlocks[_freeIndex++] = (size_t)node;
return true;
}