C++內(nèi)存池兩種方案解析
C++內(nèi)存池
前言:
使用new expression為類的多個實例分配動態(tài)內(nèi)存時,cookie導(dǎo)致內(nèi)存利用率可能不高,此時我們通過實現(xiàn)類的內(nèi)存池來降低overhead。從不成熟到巧妙優(yōu)化的內(nèi)存池,得益于union的分時復(fù)用特性,內(nèi)存利用率得到了提高。
1、C++內(nèi)存池分析
在實例化某個類的對象時(在heap而不是stack中),若不使用array new
,則每次實例化時都要調(diào)用一次內(nèi)存分配函數(shù),類的每個實例在內(nèi)存中都有上下兩個cookie,從而降低了內(nèi)存的利用率。然而,array new
也有先天的缺陷,即只能調(diào)用默認無參構(gòu)造函數(shù),這對于很多沒有提供無參構(gòu)造函數(shù)的類來說是不合適的。
因此,我們可以對于一個沒有實例化的類第一次實例化時,先分配一大塊內(nèi)存(內(nèi)存池),這一大塊內(nèi)存記錄在類中,只有上下兩個cookie,能夠容納多個實例。后續(xù)實例化時,若內(nèi)存池中還有剩余內(nèi)存,則不必申請內(nèi)存分配,只在內(nèi)存池中分配。內(nèi)存回收時,將實例所占用的內(nèi)存回收到內(nèi)存池中。若內(nèi)存池中無內(nèi)存,則再申請分配大塊內(nèi)存。
2、多此一舉方案
我們以鏈表的形式組織內(nèi)存池,內(nèi)存池中鏈表的每個結(jié)點是一個小桶,這個桶中裝我們實例化的對象。
內(nèi)存池鏈表的頭結(jié)點記錄在類中,即以class staic變量的形式存儲。組織形式如下:
實現(xiàn)代碼如下:
#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;//設(shè)定內(nèi)存池能容納24個DemoClass對象 void* DemoClass::operator new(size_t size){ DemoClass* p; if(!freeMemHeader){//freeMemHeader為空,內(nèi)存池中無空間,分配內(nèi)存 size_t pool_mem_bytes = size * POOL_SIZE;//內(nèi)存池的字節(jié)大小 = 每個實例的大小(字節(jié)數(shù))* 內(nèi)存池中能容納的最大實例數(shù) freeMemHeader = reinterpret_cast<DemoClass*>(new char[pool_mem_bytes]);//new char[]分配pool_mem_bytes個字節(jié),因為每個char占用1個字節(jié) cout << "Info:向操作系統(tǒng)申請了" << pool_mem_bytes << "字節(jié)的內(nèi)存。" << endl; for(int i = 0;i < POOL_SIZE - 1; ++i){//將內(nèi)存池中POOL_SIZE個小塊內(nèi)存,串起來。 freeMemHeader[i].next = &freeMemHeader[i + 1]; } freeMemHeader[POOL_SIZE - 1].next = nullptr; } p = freeMemHeader;//取內(nèi)存池(鏈表)的頭部,分配給要實例化的對象 cout << "Info:從內(nèi)存池中取了" << size << "字節(jié)的內(nèi)存。" << endl; freeMemHeader = freeMemHeader -> next;//從內(nèi)存池中刪去取出的那一小塊地址,即更新內(nèi)存池 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; }
其結(jié)果如下:
可以看到每個DemoClass
的實例大小為24字節(jié),內(nèi)存池一次從操作系統(tǒng)中申請了576個字節(jié)的內(nèi)存,這些內(nèi)存可以容納24個實例。上面顯示出了每個實例的內(nèi)存地址,內(nèi)存池中相鄰實例的內(nèi)存首地址之差為24,即實例的大小,證明了一個內(nèi)存池的實例之間確實沒有cookie。
當(dāng)內(nèi)存池中內(nèi)存用完后,又向操作系統(tǒng)申請了576個字節(jié)的內(nèi)存。
由此,只有每個內(nèi)存池兩側(cè)有cookie,而內(nèi)存池中的實例不存在cookie,相比于每次調(diào)用new expression
實例化對象都有cookie,內(nèi)存池的組織形式確實在形式上提高了內(nèi)存利用率。
那么,有什么問題么?
sizeof(DemoClass)
等于24
:
- int data數(shù)據(jù)域占4個字節(jié)
- 兩個構(gòu)造函數(shù)一個析構(gòu)函數(shù)各占4字節(jié),共12字節(jié)
- 額外的指針DemoClass*,在64位機器上,占8個字節(jié)
這樣一個DemoClass
的大小確實是24字節(jié)。wait,what
?
我們?yōu)榱私鉀Qcookie帶來的內(nèi)存浪費,引入了指針next,但卻又引入了8個字節(jié)的overhead,脫褲子放屁,多此一舉?
這樣看來確實沒有達到要求,但至少為我們提供了一種思路,不是么?
3、分時復(fù)用改進方案
首先我們先回憶下c++ 中的Union
:
在任意時刻,聯(lián)合中只能有一個數(shù)據(jù)成員可以有值。當(dāng)給聯(lián)合中某個成員賦值之后,該聯(lián)合中的其它成員就變成未定義狀態(tài)了。
結(jié)合我們之前不成熟的內(nèi)存池,我們發(fā)現(xiàn),當(dāng)內(nèi)存池中的桶還沒有被分配給實例時,只有next域有用,而當(dāng)桶被分配給實例后,next域就沒什么用了;當(dāng)桶被回收時,數(shù)據(jù)域變無用而next指針又需要用到。這不正是union
的特性么?
看一下代碼實現(xiàn):
#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;//設(shè)定內(nèi)存池能容納24個DemoClass對象 void* DemoClass::operator new(size_t size){ DemoClass* p; if(!freeMemHeader){//freeMemHeader為空,內(nèi)存池中無空間,分配內(nèi)存 size_t pool_mem_bytes = size * POOL_SIZE;//內(nèi)存池的字節(jié)大小 = 每個實例的大?。ㄗ止?jié)數(shù))* 內(nèi)存池中能容納的最大實例數(shù) freeMemHeader = reinterpret_cast<DemoClass*>(new char[pool_mem_bytes]);//new char[]分配pool_mem_bytes個字節(jié),因為每個char占用1個字節(jié) cout << "Info:向操作系統(tǒng)申請了" << pool_mem_bytes << "字節(jié)的內(nèi)存。" << endl; for(int i = 0;i < POOL_SIZE - 1; ++i){//將內(nèi)存池中POOL_SIZE個小塊內(nèi)存,串起來。 freeMemHeader[i].next = &freeMemHeader[i + 1]; } freeMemHeader[POOL_SIZE - 1].next = nullptr; } p = freeMemHeader;//取內(nèi)存池(鏈表)的頭部,分配給要實例化的對象 cout << "Info:從內(nèi)存池中取了" << size << "字節(jié)的內(nèi)存。" << endl; freeMemHeader = freeMemHeader -> next;//從內(nèi)存池中刪去取出的那一小塊地址,即更新內(nèi)存池 p -> next = nullptr; return p; } void DemoClass::operator delete(void* p){ DemoClass* tmp = (DemoClass*) p; tmp -> next = freeMemHeader; freeMemHeader = tmp; }
對比前一種實現(xiàn)代碼,只是構(gòu)造函數(shù)、數(shù)據(jù)域和指針域的組織形式發(fā)生了變化:
- 由于數(shù)據(jù)域增加了price項,構(gòu)造函數(shù)中也增加了對應(yīng)的參數(shù)
- 數(shù)據(jù)域被集成定義成一個類自定義struct類型
- 數(shù)據(jù)域和指針域被組織為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; }
結(jié)果:
可以看到每個DemoClass
的實例大小為24字節(jié),一個內(nèi)存池的實例之間沒有cookie。
分析一下sizeof(DemoClass)
等于24
的緣由:
data數(shù)據(jù)域占12個字節(jié)(int 4字節(jié)、double 8字節(jié))。
兩個構(gòu)造函數(shù)一個析構(gòu)函數(shù)各占4字節(jié),共12字節(jié)。
指針DemoClass,在64位機器上,占8個字節(jié),但由于和數(shù)據(jù)域使用了union,data數(shù)據(jù)域12個字節(jié)中的前8個字節(jié)在適當(dāng)?shù)臅r機被看作DemoClass,而不占用額外空間,消除了overhead。
這樣一個DemoClass
的大小確實是24字節(jié)。利用union的分時復(fù)用特性,我們消除了初步方案中指針帶來的脫褲子放屁效果。
4、其他的思考
細心的讀者可能會發(fā)現(xiàn),前面的那兩種方案都有共同的小缺陷,即當(dāng)程序一直實例化而不析構(gòu)時,內(nèi)存池會向操作系統(tǒng)申請多次大塊內(nèi)存,而當(dāng)這些對象一起回收時,內(nèi)存池中的剩余桶數(shù)會遠大于設(shè)定的POOL_SIZE的大小,這個峰值多大取決于類實例化和回收的時機。
另外,內(nèi)存池中的內(nèi)存暫時不會回收給操作系統(tǒng),峰值很大可能會對內(nèi)存分配帶來一些影響,不過這卻不屬于內(nèi)存泄漏。在以后的文章中,我們可能會討論一些性能更好的內(nèi)存分配方案。
以上就是C++內(nèi)存池兩種方案對比的詳細內(nèi)容,更多關(guān)于C++內(nèi)存池的資料請關(guān)注腳本之家其它相關(guān)文章!望大家以后多多支持腳本之家!
相關(guān)文章
使用Qt實現(xiàn)監(jiān)聽網(wǎng)頁是否響應(yīng)并導(dǎo)出Excel表
Qt導(dǎo)出數(shù)據(jù)到excel,方法有很多,下面這篇文章主要給大家介紹了關(guān)于使用Qt實現(xiàn)監(jiān)聽網(wǎng)頁是否響應(yīng)并導(dǎo)出Excel表的相關(guān)資料,文中通過代碼示例介紹的非常詳細,需要的朋友可以參考下2023-11-11教你Visual?Studio?2022如何新建一個C語言工程(圖文詳解)
這篇文章主要介紹了Visual?Studio?2022如何新建一個C語言工程,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-09-09淺談Windows系統(tǒng)下C語言編程中Glib庫的使用
這篇文章主要介紹了Windows系統(tǒng)下C語言編程中Glib庫的使用,Glib庫在多線程編程中經(jīng)??梢杂玫?需要的朋友可以參考下2016-02-02C++實現(xiàn)雙目立體匹配Census算法的示例代碼
這篇文章主要為大家詳細介紹了如何利用C++實現(xiàn)雙目立體匹配Census算法,文中的示例代碼講解詳細,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2022-08-08