您好,登錄后才能下訂單哦!
這篇文章主要介紹了C++怎么實現定長內存池,具有一定借鑒價值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。
池是在計算機技術中經常使用的一種設計模式,其內涵在于:將程序中需要經常使用的核心資源先申請出來,放到一個池內,由程序自己管理,這樣可以提高資源的使用效率,也可以保證本程序占有的資源數量。 經常使用的池技術包括內存池、線程池和連接池(數據庫經常使用到)等,其中尤以內存池和線程池使用最多。
內存池(Memory Pool) 是一種動態內存分配與管理技術。 通常情況下,程序員習慣直接使用 new、delete、malloc、free 等API申請分配和釋放內存,這樣導致的后果是:當程序長時間運行時,由于所申請內存塊的大小不定,頻繁使用時會造成大量的內存碎片從而降低程序和操作系統的性能。內存池則是在真正使用內存之前,先申請分配一大塊內存(內存池)留作備用,當程序員申請內存時,從池中取出一塊動態分配,當程序員釋放內存時,將釋放的內存再放入池內,再次申請池可以 再取出來使用,并盡量與周邊的空閑內存塊合并。若內存池不夠時,則自動擴大內存池,從操作系統中申請更大的內存池。
內碎片:
內部碎片就是已經被分配出去(能明確指出屬于哪個進程)卻不能被利用的內存空間;內部碎片是處于區域內部或頁面內部的存儲塊。占有這些區域或頁面的進程并不使用這個 存儲塊。而在進程占有這塊存儲塊時,系統無法利用它。直到進程釋放它,或進程結束時,系統才有可能利用這個存儲塊。(編譯器會對數據進行對齊操作,當不是編譯器的最小對齊數的整數倍的時候需要添加一些來保證對齊,那么這塊為了對齊而添加的就是內碎片)
外碎片(通常所講的內存碎片):
假設系統依次分配了16byte、8byte、16byte、4byte,還剩余8byte未分配。這時要分配一個24byte的空間,操作系統回收了一個上面的兩個16byte,總的剩余空間有40byte,但是卻不能分配出一個連續24byte的空間,這就是外碎片問題。(本來有足夠的內存,但是由于碎片化無法申請到稍大一些的連續內存)
定位new表達式是在已分配的原始內存空間中調用構造函數初始化一個對象。使用格式:new (place_address) type或者new (place_address) type(initializer-list),place_address必須是一個指針,initializer-list是類型的初始化列表。
使用場景:
定位new表達式在實際中一般是配合內存池使用。因為內存池分配出的內存沒有初始化,所以如果是自定義類型的對象,需要**使用new的定義表達式進行顯示調構造函數**進行初始化。
即實現一個 FreeList,每個 FreeList 用于分配固定大小的內存塊,比如用于分配 32字節對象的固定內存分配器,之類的。
優點:
簡單粗暴,分配和釋放的效率高,解決實際中特定場景下的問題有效。缺點:
功能單一,只能解決定長的內存需求,另外占著內存沒有釋放。
實現的思想:
先向內存申請一塊大的內存,如果需要,那么就對這塊已經申請出來的內存進行切割(減少了和操作系統底層打交道的次數,效率也就提高了,內存池一定是可以解決申請和釋放內存的效率的)
對于不需要的小塊內存,并不是將其進行釋放掉,而是使用一個freeList將他們管理起來,如果freeList中有了空余的,那么再次申請內存首先會到自由鏈表中取,而不是去申請出來的大內存塊進行切割
對于這個申請出來的小塊內存,前4個或者8個字節存放的是下一個小內存塊的地址(這是由于在32位平臺下指針的大小是4字節,在64位平臺下指針則是8字節),這里如何巧妙的進行平臺下指針大小的適配,需要好好的進行琢磨。
(幫助理解3)指針就是地址,那么指針的類型是為了解引用取到大小,對于所申請出來的內存的類型我是不關心的,在32位平臺下我就想取出他的前4個字節,然后存放我的下一個小內存的地址,所以把obj強轉為int*類型,然后在解引用就可以拿到前4個字節。那如果在64位平臺下,就應該取其前8個字節來存放下一個小內存的地址,但是如果都寫為取前4個字節的話,這里就會發生指針越界的問題。下述代碼所寫的Nextobj()接口函數就是為了能夠取出小內存中的前4個字節或者8個字節。我需要的類型是void*,可以自動的適配平臺(類比于上述的int類型,就可以相通)
//實現一個定長的內存池(針對某一個具體的對象,所以起名字叫ObjictPool) #pragma once #include"Common.h" template<class T> class ObjectPool { public: ~ObjectPool() { // } //此時代碼還存一個很大的問題:我們默認這里取的是前四個字節,但是在64位的平臺下,需要取的應該是這塊小內存的前8個字節來保存地址 void*& Nextobj(void* obj) { return *((void**)obj); //對于返回的void*可以自動的適配平臺 } //申請內存的函數接口 T* New() { T* obj = nullptr; //一上來首先應該判斷freeList if (_freeList) { //那就直接從自由鏈表中取一塊出來 obj = (T*)_freeList; //_freeList = (void*)(*(int*)_freeList); _freeList = Nextobj(_freeList); } else { //表示自由鏈表是空的 //那么這里又要進行判斷,memory有沒有 if (_leftSize < sizeof(T)) //說明此時空間不夠了 { //那么就進行切割 _leftSize = 1024 * 100; _memory = (char*)malloc(_leftSize); //對于C++來說,如果向系統申請失敗了,則會拋異常 if (_memory == nullptr) { throw std::bad_alloc(); } } //進行memory的切割 obj = (T*)_memory; _memory += sizeof(T); //這里如果想不通可以畫一下圖,很簡單 _leftSize -= sizeof(T); //表示剩余空間的大小 } new(obj)T; //定位new,因為剛申請的空間內如果是自定義類型是沒有初始化的 //所以需要可以顯示的調用這個類型的構造函數,這個是專門配合內存池使用的 return obj; } void Delete(T* obj) { obj->~T();//先把自定義類型進行析構 //然后在進行釋放,但是此時還回來的都是一塊一塊的小內存,無法做到一次性進行free,所以需要一個自由鏈表將這些小內存都掛接住 //這里其實才是核心的關鍵點 //對于指針來說,在32位的平臺下面是4字節,在64位平臺下面是8字節 //頭插到freeList //*((int*)obj)= (int)_freeList; Nextobj(obj) = _freeList; _freeList = obj; } private: char* _memory = nullptr;//這里給char*是為了好走大小,并不是一定要給T*或者void* int _leftSize = 0; //為什么會加入這個成員變量呢?因為你的menory += sizeof(T),有可能就會造成越界的問題 void* _freeList = nullptr; //給一些缺省值,讓他的構造函數自己生成就可以了 }; struct TreeNode { int _val; TreeNode* _left; TreeNode* _right; TreeNode() :_val(0) , _left(nullptr) , _right(nullptr) {} }; void TestObjectPool() { 驗證還回來的內存是否重復利用的問題 ObjectPool<TreeNode> tnPool; TreeNode* node1 = tnPool.New(); TreeNode* node2 = tnPool.New(); cout << node1 << endl; cout << node2 << endl; tnPool.Delete(node1); TreeNode* node3 = tnPool.New(); cout << node3 << endl; cout << endl; //驗證內存池到底快不快,有沒有做到性能的優化 //new底層本身調用的malloc,會一直和操作系統的底部打交道 size_t begin1 = clock(); std::vector<TreeNode*> v1; for (int i = 0; i < 1000000; ++i) { v1.push_back(new TreeNode); } for (int i = 0; i < 1000000; ++i) { delete v1[i]; } size_t end1 = clock(); //這里我們調用自己所寫的內存池 ObjectPool<TreeNode> tnPool; size_t begin2 = clock(); std::vector<TreeNode*> v2; for (int i = 0; i < 1000000; ++i) { v2.push_back(tnPool.New()); } for (int i = 0; i < 1000000; ++i) { tnPool.Delete(v2[i]); } size_t end2 = clock(); cout << end1 - begin1 << endl; cout << end2 - begin2 << endl; }
這個定長的內存池依舊存在著大量的問題:
我們所采用的是取這塊小內存的前4個或者8個字節來存放下一個小內存的地址,但是如果這里的模板類型T是一個char類型怎么辦,它本身都沒有4字節,怎么來存放?(解決的辦法就是,進行一次判斷如果sizeof(T) < sizeof(T*)的大小,那么就開辟T*的大小)
無法編寫這個ObjectPool的析構函數,因為申請的都是一個個的小塊內存,但是對于free來說,應該是一次性的對整個所開辟出來的內存塊都進行釋放(解決的辦法就是,將這些向操作系統申請的大塊內存也管理起來,如果小塊內存都還回來了,那么就可以對這個大塊內存進行釋放)
感謝你能夠認真閱讀完這篇文章,希望小編分享的“C++怎么實現定長內存池”這篇文章對大家有幫助,同時也希望大家多多支持億速云,關注億速云行業資訊頻道,更多相關知識等著你來學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。