C++手寫內存池的案例詳解
引言
使用new expression
為類的多個實例分配動態內存時,cookie導致內存利用率可能不高,此時我們通過實現類的內存池來降低overhead。從不成熟到巧妙優化的內存池,得益於union的分時復用特性,內存利用率得到瞭提高。
原因
在實例化某個類的對象時(在heap而不是stack中),若不使用array new
,則每次實例化時都要調用一次內存分配函數,類的每個實例在內存中都有上下兩個cookie,從而降低瞭內存的利用率。然而,array new
也有先天的缺陷,即隻能調用默認無參構造函數,這對於很多沒有提供無參構造函數的類來說是不合適的。
因此,我們可以對於一個沒有實例化的類第一次實例化時,先分配一大塊內存(內存池),這一大塊內存記錄在類中,隻有上下兩個cookie,能夠容納多個實例。後續實例化時,若內存池中還有剩餘內存,則不必申請內存分配,隻在內存池中分配。內存回收時,將實例所占用的內存回收到內存池中。若內存池中無內存,則再申請分配大塊內存。
脫褲子放屁方案
我們以鏈表的形式組織內存池,內存池中每個一個鏈表是一個小桶,這個桶中裝我們實例化的對象。
內存池鏈表的頭結點記錄在類中,即以class staic變量的形式存儲。組織形式如下:
實現代碼如下:
#include <iostream> using namespace std; class DemoClass{ public: DemoClass() = default; DemoClass(int i):data(i){} static void* operator new(size_t size); static void operator delete(void *); virtual ~DemoClass(){} private: DemoClass *next; int data; static DemoClass *freeMemHeader; static const size_t POOL_SIZE; }; DemoClass * DemoClass::freeMemHeader = nullptr; const size_t DemoClass::POOL_SIZE = 24;//設定內存池能容納24個DemoClass對象 void* DemoClass::operator new(size_t size){ DemoClass* p; if(!freeMemHeader){//freeMemHeader為空,內存池中無空間,分配內存 size_t pool_mem_bytes = size * POOL_SIZE;//內存池的字節大小 = 每個實例的大小(字節數)* 內存池中能容納的最大實例數 freeMemHeader = reinterpret_cast<DemoClass*>(new char[pool_mem_bytes]);//new char[]分配pool_mem_bytes個字節,因為每個char占用1個字節 cout << "Info:向操作系統申請瞭" << pool_mem_bytes << "字節的內存。" << endl; for(int i = 0;i < POOL_SIZE - 1; ++i){//將內存池中POOL_SIZE個小塊內存,串起來。 freeMemHeader[i].next = &freeMemHeader[i + 1]; } freeMemHeader[POOL_SIZE - 1].next = nullptr; } p = freeMemHeader;//取內存池(鏈表)的頭部,分配給要實例化的對象 cout << "Info:從內存池中取瞭" << size << "字節的內存。" << endl; freeMemHeader = freeMemHeader -> next;//從內存池中刪去取出的那一小塊地址,即更新內存池 p -> next = nullptr; return p; } void DemoClass::operator delete(void* p){ DemoClass* tmp = (DemoClass*) p; tmp -> next = freeMemHeader; freeMemHeader = tmp; }
測試代碼如下:
int main(int argc, char* argv[]){ cout << "sizeof(DemoClass):" << sizeof(DemoClass) << endl; size_t N = 32; DemoClass* demos[N]; for(int i = 0; i < N; ++i){ demos[i] = new DemoClass(i); cout << "address of the ith demo:" << demos[i] << endl; cout << endl; } return 0; }
其結果如下:
可以看到每個DemoClass
的實例大小為24字節,內存池一次從操作系統中申請瞭576個字節的內存,這些內存可以容納24個實例。上面顯示出瞭每個實例的內存地址,內存池中相鄰實例的內存首地址之差為24,即實例的大小,證明瞭一個內存池的實例之間確實沒有cookie。
當內存池中內存用完後,又向操作系統申請瞭576個字節的內存。
由此,隻有每個內存池兩側有cookie,而內存池中的實例不存在cookie,相比於每次調用new expression
實例化對象都有cookie,內存池的組織形式確實在形式上提高瞭內存利用率。
那麼,有什麼問題麼?
sizeof(DemoClass)
等於24
:
- int data數據域占4個字節
- 兩個構造函數一個析構函數各占4字節,共12字節
- 額外的指針DemoClass*,在64位機器上,占8個字節
這樣一個DemoClass
的大小確實是24字節。wait,what?
我們為瞭解決cookie帶來的內存浪費,引入瞭指針next,但卻又引入瞭8個字節的overhead,脫褲子放屁,多此一舉?
這樣看來確實沒有達到要求,但至少為我們提供瞭一種思路,不是麼?
分時復用改進方案
首先我們先回憶下c++ 中的Union
:
在任意時刻,聯合中隻能有一個數據成員可以有值。當給聯合中某個成員賦值之後,該聯合中的其它成員就變成未定義狀態瞭。
結合我們之前不成熟的內存池,我們發現,當內存池中的桶還沒有被分配給實例時,隻有next域有用,而當桶被分配給實例後,next域就沒什麼用瞭;當桶被回收時,數據域變無用而next指針又需要用到。這不正是union
的特性麼?
看一下代碼實現:
#include <iostream> using namespace std; class DemoClass{ public: DemoClass() = default; DemoClass(int i, double p){ data.num = i; data.price = p; } static void* operator new(size_t size); static void operator delete(void *); virtual ~DemoClass(){} private: struct DemoData{ int num; double price; }; private: static DemoClass *freeMemHeader; static const size_t POOL_SIZE; union { DemoClass *next; DemoData data; }; }; DemoClass * DemoClass::freeMemHeader = nullptr; const size_t DemoClass::POOL_SIZE = 24;//設定內存池能容納24個DemoClass對象 void* DemoClass::operator new(size_t size){ DemoClass* p; if(!freeMemHeader){//freeMemHeader為空,內存池中無空間,分配內存 size_t pool_mem_bytes = size * POOL_SIZE;//內存池的字節大小 = 每個實例的大小(字節數)* 內存池中能容納的最大實例數 freeMemHeader = reinterpret_cast<DemoClass*>(new char[pool_mem_bytes]);//new char[]分配pool_mem_bytes個字節,因為每個char占用1個字節 cout << "Info:向操作系統申請瞭" << pool_mem_bytes << "字節的內存。" << endl; for(int i = 0;i < POOL_SIZE - 1; ++i){//將內存池中POOL_SIZE個小塊內存,串起來。 freeMemHeader[i].next = &freeMemHeader[i + 1]; } freeMemHeader[POOL_SIZE - 1].next = nullptr; } p = freeMemHeader;//取內存池(鏈表)的頭部,分配給要實例化的對象 cout << "Info:從內存池中取瞭" << size << "字節的內存。" << endl; freeMemHeader = freeMemHeader -> next;//從內存池中刪去取出的那一小塊地址,即更新內存池 p -> next = nullptr; return p; } void DemoClass::operator delete(void* p){ DemoClass* tmp = (DemoClass*) p; tmp -> next = freeMemHeader; freeMemHeader = tmp; }
對比前一種實現代碼,隻是構造函數、數據域和指針域的組織形式發生瞭變化:
- 由於數據域增加瞭price項,構造函數中也增加瞭對應的參數
- 數據域被集成定義成一個類自定義struct類型
- 數據域和指針域被組織為union
測試代碼依舊:
int main(int argc, char* argv[]){ cout << "sizeof(DemoClass):" << sizeof(DemoClass) << endl; size_t N = 32; DemoClass* demos[N]; for(int i = 0; i < N; ++i){ demos[i] = new DemoClass(i, i * i); cout << "address of the " << i << "th demo:" << demos[i] << endl; cout << endl; } return 0; }
結果:
可以看到每個DemoClass
的實例大小為24字節,一個內存池的實例之間沒有cookie。
分析一下sizeof(DemoClass)
等於24
的緣由:
- data數據域占12個字節(int 4字節、double 8字節)。
- 兩個構造函數一個析構函數各占4字節,共12字節。
- 指針DemoClass,在64位機器上,占8個字節,但由於和數據域使用瞭union,data數據域12個字節中的前8個字節在適當的時機被看作DemoClass,而不占用額外空間,消除瞭overhead。
這樣一個DemoClass
的大小確實是24字節。利用union的分時復用特性,我們消除瞭初步方案中指針帶來的脫褲子放屁效果。
另外的思考
細心的讀者可能會發現,前面的那兩種方案都有共同的小缺陷,即當程序一直實例化而不析構時,內存池會向操作系統申請多次大塊內存,而當這些對象一起回收時,內存池中的剩餘桶數會遠大於設定的POOL_SIZE的大小,這個峰值多大取決於類實例化和回收的時機。
另外,內存池中的內存暫時不會回收給操作系統,峰值很大可能會對內存分配帶來一些影響,不過這卻不屬於內存泄漏。在以後的文章中,我們可能會討論一些性能更好的內存分配方案。
參考資料
[1] Effective C++ 3/e
[2] C++ Primer 5/e
[3] 侯捷老師的內存管理課程
到此這篇關於C++手寫內存池的文章就介紹到這瞭,更多相關C++內存池內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!