轉載:http://www.cnblogs.com/rosesmall/archive/2012/05/09.html
new的三種形態
C++語言一直被認爲是複雜編程語言中的傑出代表之一,不僅僅是因爲其繁縟的語法規則,還因爲其晦澀的術語。下面要講的就是你的老熟人—new:
它是一個內存管理的操作符,能夠從堆中劃分一塊區域,自動調用構造函數,動態地創建某種特定類型的數據,最後返回該區域的指針。該數據使用完後,應調用delete運算符,釋放動態申請的這塊內存。
如果這就是你對new的所有認識,那麼我不得不說,你依舊被new的和善外表所矇蔽着。看似簡單的new其實有着三種不同的外衣。
是的,你沒有看錯,也不用感到驚奇,一個簡單的new確實有三種不同的形態,它扮演着三種不同的角色,如下所示:
- new operator
- operator new
- placement new
下面的代碼片段展示的是我們印象中熟悉的那個new:
- string *pStr = new string("Memory Management");
- int *pInt = new int(2011);
這裏所使用的new是它的第一種形態new operator。它與sizeof有幾分類似,它是語言內建的,不能重載,也不能改變其行爲,無論何時何地它所做的有且只有以下三件事,如圖3-2所示。
圖3-2 new operator所完成的三件事 |
所以當寫出“string *pStr = new string("Memory Management");”代碼時,它其實做的就是以下幾件事:
- //爲string對象分配raw內存
- void *memory = operator new( sizeof(string) );
- //調用構造函數,初始化內存中的對象
- call string::string()on memory;
- //獲得對象指針
- string *pStr = static_cast<string*>(memory);
- 當然,對於內置類型,第二步是被忽略的,即:
- //爲int分配raw內存
- void *memory = operator new( sizeof(int) );
- //獲得對象指針
- int *pInt = static_cast<int*>(memory);
其實new operator背後還藏着一個祕密,即它在執行過程中,與其餘的兩種形態都發生了密切的關係:第一步的內存申請是通過operator new完成的;而在第二步中,關於調用什麼構造函數,則由new的另外一種形態placement new來決定的。
對於new的第二種形態—內存申請中所調用的operator new,它只是一個長着“明星臉”的普通運算符,具有和加減乘除操作符一樣的地位,因此它也是可以重載的。
operator new在默認情況下首先會調用分配內存的代碼,嘗試從堆上得到一段空間,同時它對事情的結果做了最充分的準備:如果成功則直接返回;否則,就轉而去調用一 個new_hander,然後繼續重複前面過程,直到異常拋出爲止。所以如果operator new要返回,必須滿足以下條件之一:
內存成功分配。
拋出bad_alloc異常。
通常,operator new函數通過以下方式進行聲明:
- void* operator new(size_t size);
注意,這個函數的返回值類型是void*,因爲這個函數返回的是一個未經處理的指針,是一塊未初始化的內存,它像極了C庫中的malloc函數。如果你對這個過程不滿意,那麼可以通過重載operator new來進行必要的干預。例如:
- class A
- {
- public:
- A(int a);
- ~A();
- void* operator new(size_t size);
- ...
- };
- void* A::operator new(size_t size)
- {
- cout<<"Our operator new...");
- return ::operator new(size);
- }
這裏的operator new調用了全局的new來進行內存分配(::operator new(size))。當然這裏的全局new也是可以重載的,但是在全局空間中重載void * operator new(size_t size)函數將會改變所有默認的operator new的行爲方式,所以必須十二分的注意。還有一點需要注意的是,正像new與delete一一對應一樣,operator new和operator delete也是一一對應的;如果重載了operator new,那麼也得重載對應的operator delete。
最後,要介紹的是new的第三種形態—placement new。正如前面所說的那樣,placement new是用來實現定位構造的,可以通過它來選擇合適的構造函數。雖然通常情況下,構造函數是由編譯器自動調用的,但是不排除你有時確實想直接手動調用,比 如對未初始化的內存進行處理,獲取想要的對象,此時就得求助於一個叫做placement new的特殊的operator new了:
- #include <new>
- #include "ClassA.h"
- int main()
- {
- void *s = operator new(sizeof(A));
- A* p = (A*)s;
- new(p) A(2011); //p->A::A(2011);
- ... // processing code
- return 0;
- }
placement new是標準C++庫的一部分,被聲明在了頭文件中,所以只有包含了這個文件,我們才能使用它。它在文件中的函數定義很簡單,如下所示:
- #ifndef __PLACEMENT_NEW_INLINE
- #define __PLACEMENT_NEW_INLINE
- inline void *__CRTDECL operator new(size_t, void *_Where) _THROW0()
- { // construct array with placement at _Where
- return (_Where);
- }
- inline void __CRTDECL operator delete(void *, void *) _THROW0()
- { // delete if placement new fails
- }
- #endif /* __PLACEMENT_NEW_INLINE */
這就是placement new需要完成的事。細心的你可能會發現,placement new的定義與operator new聲明之間的區別:placement new的定義多一個void*參數。使用它有一個前提,就是已經獲得了指向內存的指針,因爲只有這樣我們才知道該把placement new初始化完成的對象放在哪裏。
在使用placement new的過程中,我們看到的卻是"new(p) A(2011)"這樣奇怪的調用形式,它在特定的內存地址上用特定的構造函數實現了構造一個對象的功能,A(2011)就是對構造函數A(int a)的顯式調用。當然,如果顯式地調用placement new,那麼也得本着負責任的態度顯式地調用與之對應的placement delete:p->~A();。這部分工作本來可以由編譯器獨自完成的:在使用new operator的時候,編譯器會自動生成調用placement new的代碼,相應的,在調用delete operator時同樣會生成調用析構函數的代碼。所以,除非特別必要,不要直接使用placement new。但是要清楚,它是new operator的一個不可或缺的步驟。當默認的new operator對內存的管理不能滿足我們的需要,希望自己手動管理內存時,placement new就變得有用了。就像STL中的allocator一樣,它藉助placement new來實現更靈活有效的內存管理。
最後,總結一下:
如果是在堆上建立對象,那麼應該使用 new operator,它會爲你提供最爲周全的服務。
如果僅僅是分配內存,那麼應該調用operator new,但初始化不在它的工作職責之內。如果你對默認的內存分配過程不滿意,想單獨定製,重載operator new 是不二選擇。
如果想在一塊已經獲得的內存裏建立一個對象,那就應該用placement new。但是通常情況下不建議使用,除非是在某些對時間要求非常高的應用中,因爲相對於其他兩個步驟,選擇合適的構造函數完成對象初始化是一個時間相對較長的過程。
請記住:
不要自信地認爲自己對new很熟悉,要正確區分new所具有的三種不同形態,並能在合適的情形下選擇合適的形態,以滿足特定需求。
posted @ 2012-05-09 16:49 笑笑小白 閱讀(38) 評論(0) 編輯
轉C++內存池實現
#pragma once
#ifndef _MEMORY_POOL_
#define _MEMORY_POOL_
#include <list>
#include <Windows.h>
using std::list;
template <typename Type>
class MemoryPool
{
private:
int m_nMaxCount;
int m_nFreeCount;
list <Type*> m_pMemList;
Type * m_pType;
CRITICAL_SECTION m_csMemLock;
public:
MemoryPool(int nMax);
~MemoryPool();
Type* New();
void Delete(Type* p);
int GetFreeCount();
bool IsFull();
};
template <typename Type>
MemoryPool<Type>::MemoryPool(int nMax)
{
m_pType=NULL;
m_pType=new (std::nothrow) Type[nMax];
if (m_pType!=NULL)
{
for (int i = 0; i < nMax; ++i)
{
m_pMemList.push_back(&m_pType[i]);
}
m_nMaxCount=nMax;
m_nFreeCount=m_nMaxCount;
}
else
{
m_nMaxCount=0;
m_nFreeCount=0;
}
InitializeCriticalSection(&m_csMemLock);
}
template <typename Type>
inline MemoryPool <Type>::~MemoryPool()
{
delete[] m_pType;
DeleteCriticalSection(&m_csMemLock);
}
template <typename Type>
Type* MemoryPool <Type>::New()
{
Type* pNew=NULL;
if (m_pType != NULL && m_nFreeCount > 0)
{
EnterCriticalSection(&m_csMemLock);
pNew=m_pMemList.front();
m_pMemList.pop_front();
LeaveCriticalSection(&m_csMemLock);
--m_nFreeCount;
}
return pNew;
}
template <typename Type>
void MemoryPool <Type>::Delete(Type* p)
{
bool bIsValaidPointer = false;
for (int i = 0;i < m_nMaxCount;++i)
{
if (&m_pType[i] == p)
{
//判斷p是否是內存池中的內存指針,防止傳入其他外部自己new的內存在內部釋放
bIsValaidPointer=true;
}
}
list<Type*>::iterator iter;
for (iter = m_pMemList.begin();iter != m_pMemList.end();++iter)
{
if (*iter==p)
{
//判斷p是否已經被釋放過了,防止對同一指針多次釋放
bIsValaidPointer = false;
}
}
if (p != NULL && m_pType != NULL && m_nFreeCount < m_nMaxCount && bIsValaidPointer)
{
EnterCriticalSection(&m_csMemLock);
m_pMemList.push_front(p);
LeaveCriticalSection(&m_csMemLock);
++m_nFreeCount;
}
}
template <typename Type>
inline int MemoryPool<Type>::GetFreeCount() //獲取剩餘容量
{
return m_nFreeCount;
}
template <typename Type>
inline bool MemoryPool <Type>::IsFull()
{
return m_nFreeCount == 0?true::false;
}
#endif
posted @ 2012-05-09 14:52 笑笑小白 閱讀(78) 評論(0) 編輯
轉:自定義內存池的使用
如前所述,讀者已經瞭解到"堆"和"棧"的區別。而在編程實踐中,不可避免地要大量用到堆上的內存。例如在程序中維護一個鏈表的數據結構時,每次新 增或者刪除一個鏈表的節點,都需要從內存堆上分配或者釋放一定的內存;在維護一個動態數組時,如果動態數組的大小不能滿足程序需要時,也要在內存堆上分配 新的內存空間。
利用默認的內存管理函數new/delete或malloc/free在堆上分配和釋放內存會有一些額外的開銷。
系統在接收到分配一定大小內存的請求時,首先查找內部維護的內存空閒塊表,並且需要根據一定的算法(例如分配最先找到的不小於申請大小的內存塊給請 求者,或者分配最適於申請大小的內存塊,或者分配最大空閒的內存塊等)找到合適大小的空閒內存塊。如果該空閒內存塊過大,還需要切割成已分配的部分和較小 的空閒塊。然後系統更新內存空閒塊表,完成一次內存分配。類似地,在釋放內存時,系統把釋放的內存塊重新加入到空閒內存塊表中。如果有可能的話,可以把相 鄰的空閒塊合併成較大的空閒塊。
默認的內存管理函數還考慮到多線程的應用,需要在每次分配和釋放內存時加鎖,同樣增加了開銷。
可見,如果應用程序頻繁地在堆上分配和釋放內存,則會導致性能的損失。並且會使系統中出現大量的內存碎片,降低內存的利用率。
默認的分配和釋放內存算法自然也考慮了性能,然而這些內存管理算法的通用版本爲了應付更復雜、更廣泛的情況,需要做更多的額外工作。而對於某一個具體的應用程序來說,適合自身特定的內存分配釋放模式的自定義內存池則可以獲得更好的性能。
自定義內存池的思想通過這個"池"字表露無疑,應用程序可以通過系統的內存分配調用預先一次性申請適當大小的內存作爲一個內存池,之後應用程序自己 對內存的分配和釋放則可以通過這個內存池來完成。只有當內存池大小需要動態擴展時,才需要再調用系統的內存分配函數,其他時間對內存的一切操作都在應用程 序的掌控之中。
應用程序自定義的內存池根據不同的適用場景又有不同的類型。
從線程安全的角度來分,內存池可以分爲單線程內存池和多線程內存池。單線程內存池整個生命週期只被一個線程使用,因而不需要考慮互斥訪問的問題;多 線程內存池有可能被多個線程共享,因此則需要在每次分配和釋放內存時加鎖。相對而言,單線程內存池性能更高,而多線程內存池適用範圍更廣。
從內存池可分配內存單元大小來分,可以分爲固定內存池和可變內存池。所謂固定內存池是指應用程序每次從內存池中分配出來的內存單元大小事先已經確定,是固定不變的;而可變內存池則每次分配的內存單元大小可以按需變化,應用範圍更廣,而性能比固定內存池要低。
下面以固定內存池爲例說明內存池的工作原理,如圖6-1所示。
固定內存池由一系列固定大小的內存塊組成,每一個內存塊又包含了固定數量和大小的內存單元。
如圖6-1所示,該內存池一共包含4個內存塊。在內存池初次生成時,只向系統申請了一個內存塊,返回的指針作爲整個內存池的頭指針。之後隨着應用程 序對內存的不斷需求,內存池判斷需要動態擴大時,纔再次向系統申請新的內存塊,並把所有這些內存塊通過指針鏈接起來。對於操作系統來說,它已經爲該應用程 序分配了4個等大小的內存塊。由於是大小固定的,所以分配的速度比較快;而對於應用程序來說,其內存池開闢了一定大小,內存池內部卻還有剩餘的空間。
例如放大來看第4個內存塊,其中包含一部分內存池塊頭信息和3個大小相等的內存池單元。單元1和單元3是空閒的,單元2已經分配。當應用程序需要通 過該內存池分配一個單元大小的內存時,只需要簡單遍歷所有的內存池塊頭信息,快速定位到還有空閒單元的那個內存池塊。然後根據該塊的塊頭信息直接定位到第 1個空閒的單元地址,把這個地址返回,並且標記下一個空閒單元即可;當應用程序釋放某一個內存池單元時,直接在對應的內存池塊頭信息中標記該內存單元爲空 閒單元即可。
可見與系統管理內存相比,內存池的操作非常迅速,它在性能優化方面的優點主要如下。
(1)針對特殊情況,例如需要頻繁分配釋放固定大小的內存對象時,不需要複雜的分配算法和多線程保護。也不需要維護內存空閒表的額外開銷,從而獲得較高的性能。
(2)由於開闢一定數量的連續內存空間作爲內存池塊,因而一定程度上提高了程序局部性,提升了程序性能。
(3)比較容易控制頁邊界對齊和內存字節對齊,沒有內存碎片的問題。
本節分析在某個大型應用程序實際應用到的一個內存池實現,並詳細講解其使用方法與工作原理。這是一個應用於單線程環境且分配單元大小固定的內存池,一般用來爲執行時會動態頻繁地創建且可能會被多次創建的類對象或者結構體分配內存。
本節首先講解該內存池的數據結構聲明及圖示,接着描述其原理及行爲特徵。然後逐一講解實現細節,最後介紹如何在實際程序中應用此內存池,並與使用普通內存函數申請內存的程序性能作比較。
內存池類MemoryPool的聲明如下:
class MemoryPool { private: MemoryBlock* pBlock; USHORT nUnitSize; USHORT nInitSize; USHORT nGrowSize; public: MemoryPool( USHORT nUnitSize, USHORT nInitSize = 1024, USHORT nGrowSize = 256 ); ~MemoryPool(); void* Alloc(); void Free( void* p ); }; |
MemoryBlock爲內存池中附着在真正用來爲內存請求分配內存的內存塊頭部的結構體,它描述了與之聯繫的內存塊的使用信息:
struct MemoryBlock { USHORT nSize; USHORT nFree; USHORT nFirst; USHORT nDummyAlign1; MemoryBlock* pNext; char aData[1]; static void* operator new(size_t, USHORT nTypes, USHORT nUnitSize) { return ::operator new(sizeof(MemoryBlock) + nTypes * nUnitSize); } static void operator delete(void *p, size_t) { ::operator delete (p); } MemoryBlock (USHORT nTypes = 1, USHORT nUnitSize = 0); ~MemoryBlock() {} }; |
此內存池的數據結構如圖6-2所示。
此內存池的總體機制如下。
(1)在運行過程中,MemoryPool內存池可能會有多個用來滿足內存申請請求的內存塊,這些內存塊是從進程堆中開闢的一個較大的連續內存區 域,它由一個MemoryBlock結構體和多個可供分配的內存單元組成,所有內存塊組成了一個內存塊鏈表,MemoryPool的pBlock是這個鏈 表的頭。對每個內存塊,都可以通過其頭部的MemoryBlock結構體的pNext成員訪問緊跟在其後面的那個內存塊。
(2)每個內存塊由兩部分組成,即一個MemoryBlock結構體和多個內存分配單元。這些內存分配單元大小固定(由MemoryPool的 nUnitSize表示),MemoryBlock結構體並不維護那些已經分配的單元的信息;相反,它只維護沒有分配的自由分配單元的信息。它有兩個成員 比較重要:nFree和nFirst。nFree記錄這個內存塊中還有多少個自由分配單元,而nFirst則記錄下一個可供分配的單元的編號。每一個自由 分配單元的頭兩個字節(即一個USHORT型值)記錄了緊跟它之後的下一個自由分配單元的編號,這樣,通過利用每個自由分配單元的頭兩個字節,一個 MemoryBlock中的所有自由分配單元被鏈接起來。
(3)當有新的內存請求到來時,MemoryPool會通過pBlock遍歷MemoryBlock鏈表,直到找到某個MemoryBlock所在 的內存塊,其中還有自由分配單元(通過檢測MemoryBlock結構體的nFree成員是否大於0)。如果找到這樣的內存塊,取得其 MemoryBlock的nFirst值(此爲該內存塊中第1個可供分配的自由單元的編號)。然後根據這個編號定位到該自由分配單元的起始位置(因爲所有 分配單元大小固定,因此每個分配單元的起始位置都可以通過編號分配單元大小來偏移定位),這個位置就是用來滿足此次內存申請請求的內存的起始地址。但在返 回這個地址前,需要首先將該位置開始的頭兩個字節的值(這兩個字節值記錄其之後的下一個自由分配單元的編號)賦給本內存塊的MemoryBlock的 nFirst成員。這樣下一次的請求就會用這個編號對應的內存單元來滿足,同時將此內存塊的MemoryBlock的nFree遞減1,然後纔將剛纔定位 到的內存單元的起始位置作爲此次內存請求的返回地址返回給調用者。
(4)如果從現有的內存塊中找不到一個自由的內存分配單元(當第1次請求內存,以及現有的所有內存塊中的所有內存分配單元都已經被分配時會發生這種 情形),MemoryPool就會從進程堆中申請一個內存塊(這個內存塊包括一個MemoryBlock結構體,及緊鄰其後的多個內存分配單元,假設內存 分配單元的個數爲n,n可以取值MemoryPool中的nInitSize或者nGrowSize),申請完後,並不會立刻將其中的一個分配單元分配出 去,而是需要首先初始化這個內存塊。初始化的操作包括設置MemoryBlock的nSize爲所有內存分配單元的大小(注意,並不包括 MemoryBlock結構體的大小)、nFree爲n-1(注意,這裏是n-1而不是n,因爲此次新內存塊就是爲了滿足一次新的內存請求而申請的,馬上 就會分配一塊自由存儲單元出去,如果設爲n-1,分配一個自由存儲單元后無須再將n遞減1),nFirst爲1(已經知道nFirst爲下一個可以分配的 自由存儲單元的編號。爲1的原因與nFree爲n-1相同,即立即會將編號爲0的自由分配單元分配出去。現在設爲1,其後不用修改nFirst的 值),MemoryBlock的構造需要做更重要的事情,即將編號爲0的分配單元之後的所有自由分配單元鏈接起來。如前所述,每個自由分配單元的頭兩個字 節用來存儲下一個自由分配單元的編號。另外,因爲每個分配單元大小固定,所以可以通過其編號和單元大小(MemoryPool的nUnitSize成員) 的乘積作爲偏移值進行定位。現在唯一的問題是定位從哪個地址開始?答案是MemoryBlock的aData[1]成員開始。因爲aData[1]實際上 是屬於MemoryBlock結構體的(MemoryBlock結構體的最後一個字節),所以實質上,MemoryBlock結構體的最後一個字節也用做 被分配出去的分配單元的一部分。因爲整個內存塊由MemoryBlock結構體和整數個分配單元組成,這意味着內存塊的最後一個字節會被浪費,這個字節在 圖6-2中用位於兩個內存的最後部分的濃黑背景的小塊標識。確定了分配單元的起始位置後,將自由分配單元鏈接起來的工作就很容易了。即從aData位置開 始,每隔nUnitSize大小取其頭兩個字節,記錄其之後的自由分配單元的編號。因爲剛開始所有分配單元都是自由的,所以這個編號就是自身編號加1,即 位置上緊跟其後的單元的編號。初始化後,將此內存塊的第1個分配單元的起始地址返回,已經知道這個地址就是aData。
(5)當某個被分配的單元因爲delete需要回收時,該單元並不會返回給進程堆,而是返回給MemoryPool。返回時,MemoryPool 能夠知道該單元的起始地址。這時,MemoryPool開始遍歷其所維護的內存塊鏈表,判斷該單元的起始地址是否落在某個內存塊的地址範圍內。如果不在所 有內存地址範圍內,則這個被回收的單元不屬於這個MemoryPool;如果在某個內存塊的地址範圍內,那麼它會將這個剛剛回收的分配單元加到這個內存塊 的MemoryBlock所維護的自由分配單元鏈表的頭部,同時將其nFree值遞增1。回收後,考慮到資源的有效利用及後續操作的性能,內存池的操作會 繼續判斷:如果此內存塊的所有分配單元都是自由的,那麼這個內存塊就會從MemoryPool中被移出並作爲一個整體返回給進程堆;如果該內存塊中還有非 自由分配單元,這時不能將此內存塊返回給進程堆。但是因爲剛剛有一個分配單元返回給了這個內存塊,即這個內存塊有自由分配單元可供下次分配,因此它會被移 到MemoryPool維護的內存塊的頭部。這樣下次的內存請求到來,MemoryPool遍歷其內存塊鏈表以尋找自由分配單元時,第1次尋找就會找到這 個內存塊。因爲這個內存塊確實有自由分配單元,這樣可以減少MemoryPool的遍歷次數。
綜上所述,每個內存池(MemoryPool)維護一個內存塊鏈表(單鏈表),每個內存塊由一個維護該內存塊信息的塊頭結構 (MemoryBlock)和多個分配單元組成,塊頭結構MemoryBlock則進一步維護一個該內存塊的所有自由分配單元組成的"鏈表"。這個鏈表不 是通過"指向下一個自由分配單元的指針"鏈接起來的,而是通過"下一個自由分配單元的編號"鏈接起來,這個編號值存儲在該自由分配單元的頭兩個字節中。另 外,第1個自由分配單元的起始位置並不是MemoryBlock結構體"後面的"第1個地址位置,而是MemoryBlock結構體"內部"的最後一個字 節aData(也可能不是最後一個,因爲考慮到字節對齊的問題),即分配單元實際上往前面錯了一位。又因爲MemoryBlock結構體後面的空間剛好是 分配單元的整數倍,這樣依次錯位下去,內存塊的最後一個字節實際沒有被利用。這麼做的一個原因也是考慮到不同平臺的移植問題,因爲不同平臺的對齊方式可能 不盡相同。即當申請MemoryBlock大小內存時,可能會返回比其所有成員大小總和還要大一些的內存。最後的幾個字節是爲了"補齊",而使得 aData成爲第1個分配單元的起始位置,這樣在對齊方式不同的各種平臺上都可以工作。
有了上述的總體印象後,本節來仔細剖析其實現細節。
(1)MemoryPool的構造如下:
MemoryPool::MemoryPool( USHORT _nUnitSize, USHORT _nInitSize, USHORT _nGrowSize ) { pBlock = NULL; ① nInitSize = _nInitSize; ② nGrowSize = _nGrowSize; ③ if ( _nUnitSize > 4 ) nUnitSize = (_nUnitSize + (MEMPOOL_ALIGNMENT-1)) & ~(MEMPOOL_ALIGNMENT-1); ④ else if ( _nUnitSize <= 2 ) nUnitSize = 2; ⑤ else nUnitSize = 4; } |
從①處可以看出,MemoryPool創建時,並沒有立刻創建真正用來滿足內存申請的內存塊,即內存塊鏈表剛開始時爲空。
②處和③處分別設置"第1次創建的內存塊所包含的分配單元的個數",及"隨後創建的內存塊所包含的分配單元的個數",這兩個值在MemoryPool創建時通過參數指定,其後在該MemoryPool對象生命週期中一直不變。
後面的代碼用來設置nUnitSize,這個值參考傳入的_nUnitSize參數。但是還需要考慮兩個因素。如前所述,每個分配單元在自由狀態 時,其頭兩個字節用來存放"其下一個自由分配單元的編號"。即每個分配單元"最少"有"兩個字節",這就是⑤處賦值的原因。④處是將大於4個字節的大小 _nUnitSize往上"取整到"大於_nUnitSize的最小的MEMPOOL_ ALIGNMENT的倍數(前提是MEMPOOL_ALIGNMENT爲2的倍數)。如_nUnitSize爲11 時,MEMPOOL_ALIGNMENT爲8,nUnitSize爲16;MEMPOOL_ALIGNMENT爲4,nUnitSize爲 12;MEMPOOL_ALIGNMENT爲2,nUnitSize爲12,依次類推。
(2)當向MemoryPool提出內存請求時:
void* MemoryPool::Alloc() { if ( !pBlock ) ① { …… } MemoryBlock* pMyBlock = pBlock; while (pMyBlock && !pMyBlock->nFree )② pMyBlock = pMyBlock->pNext; if ( pMyBlock ) ③ { char* pFree = pMyBlock->aData+(pMyBlock->nFirst*nUnitSize); pMyBlock->nFirst = *((USHORT*)pFree); pMyBlock->nFree--; return (void*)pFree; } else ④ { if ( !nGrowSize ) return NULL; pMyBlock = new(nGrowSize, nUnitSize) FixedMemBlock(nGrowSize, nUnitSize); if ( !pMyBlock ) return NULL; pMyBlock->pNext = pBlock; pBlock = pMyBlock; return (void*)(pMyBlock->aData); } } |
MemoryPool滿足內存請求的步驟主要由四步組成。
①處首先判斷內存池當前內存塊鏈表是否爲空,如果爲空,則意味着這是第1次內存申請請求。這時,從進程堆中申請一個分配單元個數爲 nInitSize的內存塊,並初始化該內存塊(主要初始化MemoryBlock結構體成員,以及創建初始的自由分配單元鏈表,下面會詳細分析其代 碼)。如果該內存塊申請成功,並初始化完畢,返回第1個分配單元給調用函數。第1個分配單元以MemoryBlock結構體內的最後一個字節爲起始地址。
②處的作用是當內存池中已有內存塊(即內存塊鏈表不爲空)時遍歷該內存塊鏈表,尋找還有"自由分配單元"的內存塊。
③處檢查如果找到還有自由分配單元的內存塊,則"定位"到該內存塊現在可以用的自由分配單元處。"定位"以MemoryBlock結構體內的最後一 個字節位置aData爲起始位置,以MemoryPool的nUnitSize爲步長來進行。找到後,需要修改MemoryBlock的nFree信息 (剩下來的自由分配單元比原來減少了一個),以及修改此內存塊的自由存儲單元鏈表的信息。在找到的內存塊中,pMyBlock->nFirst爲該 內存塊中自由存儲單元鏈表的表頭,其下一個自由存儲單元的編號存放在pMyBlock->nFirst指示的自由存儲單元(亦即剛纔定位到的自由存 儲單元)的頭兩個字節。通過剛纔定位到的位置,取其頭兩個字節的值,賦給pMyBlock->nFirst,這就是此內存塊的自由存儲單元鏈表的新 的表頭,即下一次分配出去的自由分配單元的編號(如果nFree大於零的話)。修改維護信息後,就可以將剛纔定位到的自由分配單元的地址返回給此次申請的 調用函數。注意,因爲這個分配單元已經被分配,而內存塊無須維護已分配的分配單元,因此該分配單元的頭兩個字節的信息已經沒有用處。換個角度看,這個自由 分配單元返回給調用函數後,調用函數如何處置這塊內存,內存池無從知曉,也無須知曉。此分配單元在返回給調用函數時,其內容對於調用函數來說是無意義的。 因此幾乎可以肯定調用函數在用這個單元的內存時會覆蓋其原來的內容,即頭兩個字節的內容也會被抹去。因此每個存儲單元並沒有因爲需要鏈接而引入多餘的維護 信息,而是直接利用單元內的頭兩個字節,當其分配後,頭兩個字節也可以被調用函數利用。而在自由狀態時,則用來存放維護信息,即下一個自由分配單元的編 號,這是一個有效利用內存的好例子。
④處表示在②處遍歷時,沒有找到還有自由分配單元的內存塊,這時,需要重新向進程堆申請一個內存塊。因爲不是第一次申請內存塊,所以申請的內存塊包 含的分配單元個數爲nGrowSize,而不再是nInitSize。與①處相同,先做這個新申請內存塊的初始化工作,然後將此內存塊插入 MemoryPool的內存塊鏈表的頭部,再將此內存塊的第1個分配單元返回給調用函數。將此新內存塊插入內存塊鏈表的頭部的原因是該內存塊還有很多可供 分配的自由分配單元(除非nGrowSize等於1,這應該不太可能。因爲內存池的含義就是一次性地從進程堆中申請一大塊內存,以供後續的多次申請),放 在頭部可以使得在下次收到內存申請時,減少②處對內存塊的遍歷時間。
可以用圖6-2的MemoryPool來展示MemoryPool::Alloc的過程。圖6-3是某個時刻MemoryPool的內部狀態。
因爲MemoryPool的內存塊鏈表不爲空,因此會遍歷其內存塊鏈表。又因爲第1個內存塊裏有自由的分配單元,所以會從第1個內存塊中分配。檢查 nFirst,其值爲m,這時pBlock->aData+(pBlock->nFirst*nUnitSize)定位到編號爲m的自由分配 單元的起始位置(用pFree表示)。在返回pFree之前,需要修改此內存塊的維護信息。首先將nFree遞減1,然後取得pFree處開始的頭兩個字 節的值(需要說明的是,這裏aData處值爲k。其實不是這一個字節。而是以aData和緊跟其後的另外一個字節合在一起構成的一個USHORT的值,不 可誤會)。發現爲k,這時修改pBlock的nFirst爲k。然後,返回pFree。此時MemoryPool的結構如圖6-4所示。
可以看到,原來的第1個可供分配的單元(m編號處)已經顯示爲被分配的狀態。而pBlock的nFirst已經指向原來m單元下一個自由分配單元的編號,即k。
(3)MemoryPool回收內存時:
void MemoryPool::Free( void* pFree ) { …… MemoryBlock* pMyBlock = pBlock; while ( ((ULONG)pMyBlock->aData > (ULONG)pFree) || ((ULONG)pFree >= ((ULONG)pMyBlock->aData + pMyBlock->nSize)) )① { …… } pMyBlock->nFree++; ② *((USHORT*)pFree) = pMyBlock->nFirst; ③ pMyBlock->nFirst = (USHORT)(((ULONG)pFree-(ULONG)(pBlock->aData)) / nUnitSize);④ if (pMyBlock->nFree*nUnitSize == pMyBlock->nSize )⑤ { …… } else { …… } } |
如前所述,回收分配單元時,可能會將整個內存塊返回給進程堆,也可能將被回收分配單元所屬的內存塊移至內存池的內存塊鏈表的頭部。這兩個操作都需要修改鏈表結構。這時需要知道該內存塊在鏈表中前一個位置的內存塊。
①處遍歷內存池的內存塊鏈表,確定該待回收分配單元(pFree)落在哪一個內存塊的指針範圍內,通過比較指針值來確定。
運行到②處,pMyBlock即找到的包含pFree所指向的待回收分配單元的內存塊(當然,這時應該還需要檢查pMyBlock爲NULL時的情 形,即pFree不屬於此內存池的範圍,因此不能返回給此內存池,讀者可以自行加上)。這時將pMyBlock的nFree遞增1,表示此內存塊的自由分 配單元多了一個。
③處用來修改該內存塊的自由分配單元鏈表的信息,它將這個待回收分配單元的頭兩個字節的值指向該內存塊原來的第一個可分配的自由分配單元的編號。
④處將pMyBlock的nFirst值改變爲指向這個待回收分配單元的編號,其編號通過計算此單元的起始位置相對pMyBlock的aData位置的差值,然後除以步長(nUnitSize)得到。
實質上,③和④兩步的作用就是將此待回收分配單元"真正回收"。值得注意的是,這兩步實際上是使得此回收單元成爲此內存塊的下一個可分配的自由分配 單元,即將它放在了自由分配單元鏈表的頭部。注意,其內存地址並沒有發生改變。實際上,一個分配單元的內存地址無論是在分配後,還是處於自由狀態時,一直 都不會變化。變化的只是其狀態(已分配/自由),以及當其處於自由狀態時在自由分配單元鏈表中的位置。
⑤處檢查當回收完畢後,包含此回收單元的內存塊的所有單元是否都處於自由狀態,且此內存是否處於內存塊鏈表的頭部。如果是,將此內存塊整個的返回給進程堆,同時修改內存塊鏈表結構。
注意,這裏在判斷一個內存塊的所有單元是否都處於自由狀態時,並沒有遍歷其所有單元,而是判斷nFree乘以nUnitSize是否等於 nSize。nSize是內存塊中所有分配單元的大小,而不包括頭部MemoryBlock結構體的大小。這裏可以看到其用意,即用來快速檢查某個內存塊 中所有分配單元是否全部處於自由狀態。因爲只需結合nFree和nUnitSize來計算得出結論,而無須遍歷和計算所有自由狀態的分配單元的個數。
另外還需注意的是,這裏並不能比較nFree與nInitSize或nGrowSize的大小來判斷某個內存塊中所有分配單元都爲自由狀態,這是因 爲第1次分配的內存塊(分配單元個數爲nInitSize)可能被移到鏈表的後面,甚至可能在移到鏈表後面後,因爲某個時間其所有單元都處於自由狀態而被 整個返回給進程堆。即在回收分配單元時,無法判定某個內存塊中的分配單元個數到底是nInitSize還是nGrowSize,也就無法通過比較 nFree與nInitSize或nGrowSize的大小來判斷一個內存塊的所有分配單元是否都爲自由狀態。
以上面分配後的內存池狀態作爲例子,假設這時第2個內存塊中的最後一個單元需要回收(已被分配,假設其編號爲m,pFree指針指向它),如圖6-5所示。
不難發現,這時nFirst的值由原來的0變爲m。即此內存塊下一個被分配的單元是m編號的單元,而不是0編號的單元(最先分配的是最新回收的單 元,從這一點看,這個過程與棧的原理類似,即先進後出。只不過這裏的"進"意味着"回收",而"出"則意味着"分配")。相應地,m的"下一個自由單元" 標記爲0,即內存塊原來的"下一個將被分配出去的單元",這也表明最近回收的分配單元被插到了內存塊的"自由分配單元鏈表"的頭部。當然,nFree遞增 1。
處理至⑥處之前,其狀態如圖6-6所示。
這裏需要注意的是,雖然pFree被"回收",但是pFree仍然指向m編號的單元,這個單元在回收過程中,其頭兩個字節被覆寫,但其他部分的內容 並沒有改變。而且從整個進程的內存使用角度來看,這個m編號的單元的狀態仍然是"有效的"。因爲這裏的"回收"只是回收給了內存池,而並沒有回收給進程 堆,因此程序仍然可以通過pFree訪問此單元。但是這是一個很危險的操作,因爲首先該單元在回收過程中頭兩個字節已被覆寫,並且該單元可能很快就會被內 存池重新分配。因此回收後通過pFree指針對這個單元的訪問都是錯誤的,讀操作會讀到錯誤的數據,寫操作則可能會破壞程序中其他地方的數據,因此需要格 外小心。
接着,需要判斷該內存塊的內部使用情況,及其在內存塊鏈表中的位置。如果該內存塊中省略號"……"所表示的其他部分中還有被分配的單元,即nFree乘以nUnitSize不等於nSize。因爲此內存塊不在鏈表頭,因此還需要將其移到鏈表頭部,如圖6-7所示。
如果該內存塊中省略號"……"表示的其他部分中全部都是自由分配單元,即nFree乘以nUnitSize等於nSize。因爲此內存塊不在鏈表頭,所以此時需要將此內存塊整個回收給進程堆,回收後內存池的結構如圖6-8所示。
一個內存塊在申請後會初始化,主要是爲了建立最初的自由分配單元鏈表,下面是其詳細代碼:
MemoryBlock::MemoryBlock (USHORT nTypes, USHORT nUnitSize) : nSize (nTypes * nUnitSize), nFree (nTypes - 1), ④ nFirst (1), ⑤ pNext (0) { char * pData = aData; ① for (USHORT i = 1; i < nTypes; i++) ② { *reinterpret_cast<USHORT*>(pData) = i; ③ pData += nUnitSize; } } |
這裏可以看到,①處pData的初值是aData,即0編號單元。但是②處的循環中i卻是從1開始,然後在循環內部的③處將pData的頭兩個字節 值置爲i。即0號單元的頭兩個字節值爲1,1號單元的頭兩個字節值爲2,一直到(nTypes-2)號單元的頭兩個字節值爲(nTypes-1)。這意味 着內存塊初始時,其自由分配單元鏈表是從0號開始。依次串聯,一直到倒數第2個單元指向最後一個單元。
還需要注意的是,在其初始化列表中,nFree初始化爲nTypes-1(而不是nTypes),nFirst初始化爲1(而不是0)。這是因爲第 1個單元,即0編號單元構造完畢後,立刻會被分配。另外注意到最後一個單元初始並沒有設置頭兩個字節的值,因爲該單元初始在本內存塊中並沒有下一個自由分 配單元。但是從上面例子中可以看到,當最後一個單元被分配並回收後,其頭兩個字節會被設置。
圖6-9所示爲一個內存塊初始化後的狀態。
當內存池析構時,需要將內存池的所有內存塊返回給進程堆:
MemoryPool::~MemoryPool() { MemoryBlock* pMyBlock = pBlock; while ( pMyBlock ) { …… } } |
分析內存池的內部原理後,本節說明如何使用它。從上面的分析可以看到,該內存池主要有兩個對外接口函數,即Alloc和Free。Alloc返回所 申請的分配單元(固定大小內存),Free則回收傳入的指針代表的分配單元的內存給內存池。分配的信息則通過MemoryPool的構造函數指定,包括分 配單元大小、內存池第1次申請的內存塊中所含分配單元的個數,以及內存池後續申請的內存塊所含分配單元的個數等。
綜上所述,當需要提高某些關鍵類對象的申請/回收效率時,可以考慮將該類所有生成對象所需的空間都從某個這樣的內存池中開闢。在銷燬對象時,只需要 返回給該內存池。"一個類的所有對象都分配在同一個內存池對象中"這一需求很自然的設計方法就是爲這樣的類聲明一個靜態內存池對象,同時爲了讓其所有對象 都從這個內存池中開闢內存,而不是缺省的從進程堆中獲得,需要爲該類重載一個new運算符。因爲相應地,回收也是面向內存池,而不是進程的缺省堆,還需要 重載一個delete運算符。在new運算符中用內存池的Alloc函數滿足所有該類對象的內存請求,而銷燬某對象則可以通過在delete運算符中調用 內存池的Free完成。
爲了測試利用內存池後的效果,通過一個很小的測試程序可以發現採用內存池機制後耗時爲297 ms。而沒有采用內存池機制則耗時625 ms,速度提高了52.48%。速度提高的原因可以歸結爲幾點,其一,除了偶爾的內存申請和銷燬會導致從進程堆中分配和銷燬內存塊外,絕大多數的內存申請 和銷燬都由內存池在已經申請到的內存塊中進行,而沒有直接與進程堆打交道,而直接與進程堆打交道是很耗時的操作;其二,這是單線程環境的內存池,可以看到 內存池的Alloc和Free操作中並沒有加線程保護措施。因此如果類A用到該內存池,則所有類A對象的創建和銷燬都必須發生在同一個線程中。但如果類A 用到內存池,類B也用到內存池,那麼類A的使用線程可以不必與類B的使用線程是同一個線程。
另外,在第1章中已經討論過,因爲內存池技術使得同類型的對象分佈在相鄰的內存區域,而程序會經常對同一類型的對象進行遍歷操作。因此在程序運行過程中發生的缺頁應該會相應少一些,但這個一般只能在真實的複雜應用環境中進行驗證。
內存的申請和釋放對一個應用程序的整體性能影響極大,甚至在很多時候成爲某個應用程序的瓶頸。消除內存申請和釋放引起的瓶頸的方法往往是針對內存使 用的實際情況提供一個合適的內存池。內存池之所以能夠提高性能,主要是因爲它能夠利用應用程序的實際內存使用場景中的某些"特性"。比如某些內存申請與釋 放肯定發生在一個線程中,某種類型的對象生成和銷燬與應用程序中的其他類型對象要頻繁得多,等等。針對這些特性,可以爲這些特殊的內存使用場景提供量身定 做的內存池。這樣能夠消除系統提供的缺省內存機制中,對於該實際應用場景中的不必要的操作,從而提升應用程序的整體性能。
posted @ 2012-05-09 14:46 笑笑小白 閱讀(89) 評論(0) 編輯
轉:C++內存池
#ifndef _MEMPOOL_H_
#define _MEMPOOL_H_
/*
本類封裝了一個內存池,採用模板類,
模板參數就是內存池中分配的對象類型
本類主要用鏈表來實現,適用於固定大小的內存塊分配
*/
#include <vector>
using std::vector;
template<typename T>
class CMemPool
{
struct _MemNode
{
_MemNode *pPrev;
char data[sizeof(T) - sizeof(_MemNode*)];
};
struct _MemBlock
{
_MemBlock *pPrev;
_MemNode *pNode;
};
_MemBlock *m_pBlockHeader; //大內存塊鏈表頭指針
_MemNode *m_FreeNodeHeader; //空閒的小塊頭指針
int m_BlockSize; //當前小內存塊數量
private:
void AllocBlocks(); //分配內存塊
void ReallocBlocks(); //擴大內存池大小
public:
//構造函數,nInitSize爲初始內存塊數
CMemPool(int nInitSize = 128) : m_BlockSize(nInitSize), m_pBlockHeader(NULL),m_FreeNodeHeader(NULL)
{
AllocBlocks();
}
//析構函數,釋放所有底層內存塊
~CMemPool()
{
while(m_pBlockHeader != NULL)
{
_MemBlock *tmp = m_pBlockHeader;
m_pBlockHeader = tmp->pPrev;
free(tmp->pNode);
free(tmp);
}
}
/ * 下面兩個函數是主要的對外接口 */
void *AllocBlock(size_t size); //從內存池請求內存,size爲分配的內存塊大小
void FreeBlock(void *pobj , size_t size ); //把內存歸還給內存池,pobj是內存塊指針,size爲內存塊大小
//打印信息,用於調試程序
/* void PrintTestInfo()
{
printf("Handle pool: size = %d, free_index = %d, capacity = %d ", m_BlockSize,m_FreeIndex, m_Blocks.capacity());
}
*/
};
template<typename T>
void *CMemPool<T>::AllocBlock(size_t size)
{
if (size != sizeof(T)) //假如size不等於T類型大小,則使用全局分配符分配內存
{
return malloc(size);
}
if (m_FreeNodeHeader == NULL) //當前沒有空閒的內存塊,則擴大內存
{
ReallocBlocks();
}
void *p = m_FreeNodeHeader; //從空閒塊中分配一塊內存
m_FreeNodeHeader = m_FreeNodeHeader->pPrev;
return p;
}
template<typename T>
void CMemPool<T>::FreeBlock(void *pobj, size_t size)
{
if( pobj == NULL ) //pobj不能爲NULL
return ;
if( size != sizeof(T) ) //假如size不等於T類型大小,則使用全局釋放內存操作符釋放內存
{
free(pobj);
}
else //將內存歸還給內存池
{
_MemNode *p = (_MemNode*)pobj;
p->pPrev = m_FreeNodeHeader;
m_FreeNodeHeader = p;
}
}
template<typename T>
void CMemPool<T>::AllocBlocks()
{
//分配m_BlockSize大小個內存塊,放入內存池
_MemBlock *newBlock = (_MemBlock*)malloc(sizeof(_MemBlock));
//分配大塊內存
newBlock->pNode = (_MemNode*)malloc(sizeof(_MemNode) *m_BlockSize);
//將分配的大塊內存分成小塊,串接在空閒塊鏈表上
newBlock->pNode->pPrev = NULL;
for(int i = 1; i < m_BlockSize; ++i)
{
newBlock->pNode[i].pPrev = &(newBlock->pNode[i - 1]);
}
m_FreeNodeHeader = &newBlock->pNode[m_BlockSize - 1];
newBlock->pPrev = m_pBlockHeader;
m_pBlockHeader = newBlock;
}
template<typename T>
void CMemPool<T>::ReallocBlocks()
{
//將內存池擴大2倍
m_BlockSize *= 2;
AllocBlocks();
}
#endif
//--------------------------------------------------------------------------------
本內存池只適合固定大小的內存塊分配,避免頻繁分配小塊內存造成的內存碎片,同時也提高了分配內存的速度。特別適合服務器程序使用。下面有一個測試程序:
#include "MF_mempool.h"
#include "windows.h"
class Test
{
int ss;
char *p;
static CMemPool<Test> mp;
public:
static void *operator new(size_t size);
static void operator delete(void *pobj, size_t size);
};
CMemPool<Test> Test::mp(512);
void *Test::operator new(size_t size)
{
return mp.AllocBlock(size);
}
void Test::operator delete(void *pobj, size_t size)
{
mp.FreeBlock(pobj, size);
}
#define Max_Blocks 1000000
int _tmain(int argc, _TCHAR *argv[])
{
Test* *ptrarry= new Test *[Max_Blocks];
DWORD ss = GetTickCount();
for(int i = 0; i < Max_Blocks; ++i)
{
ptrarry[i] = new Test;
}
for(int j = Max_Blocks - 1; j >= Max_Blocks; --j)
{
delete ptrarry[j];
}
DWORD end = GetTickCount();
printf("take out time is %d\n", end - ss);
return 0;
}
post