告別內存碎片化,C++內存池讓程序飛起來
在C++編程的廣闊天地中,內存管理堪稱一道至關重要卻又布滿荊棘的關卡。當你精心雕琢代碼,滿心期待程序能如駿馬般奔騰馳騁時,內存問題卻常常像暗中使絆的 “小怪獸”,讓程序的性能大打折扣。你是否曾在程序運行過程中,眼睜睜看著內存占用如失控的氣球般不斷膨脹,運行速度卻越來越慢,最終陷入卡頓的泥沼?又是否在排查問題時,被復雜的內存錯誤信息折磨得焦頭爛額,卻始終找不到問題的根源?其實,這背后很大一部分原因,都與內存碎片化脫不了干系。
傳統的 C++ 內存管理方式,如使用new和delete操作符,看似簡單直接,實則暗藏隱患。在程序頻繁地進行內存分配與釋放操作時,就如同在一塊原本平整的土地上隨意地挖坑、填坑,久而久之,土地變得坑洼不平,碎片化嚴重。這不僅導致內存空間的浪費,還使得后續的內存分配操作變得困難重重,就像在崎嶇的山路上尋找一塊合適的平地建房一樣,效率極低。而此時,內存池技術宛如一道曙光,照亮了 C++ 內存管理的困境,為我們提供了一種高效、優雅的解決方案。
內存池,這個聽起來有些神秘的概念,究竟有著怎樣的魔力,能讓程序擺脫內存碎片化的束縛,實現性能的飛躍呢?接下來,就讓我們一同深入探索 C++ 內存池的奇妙世界,揭開它神秘的面紗,看看它是如何在內存管理的舞臺上大顯身手,讓我們的程序 “輕裝上陣”,飛速奔跑的。
一、c++內存池簡介
內存池是一種內存分配方式,又被稱為固定大小區塊規劃(fixed-size-blocks allocation)。通常我們習慣直接使用new、malloc等API申請分配內存,這樣做的缺點在于:由于所申請內存塊的大小不定,當頻繁使用時會造成大量的內存碎片并進而降低性能。
在內核中有不少地方內存分配不允許失敗,作為一個在這些情況下確保分配的方式,內核開發者創建了一個已知為內存池(或者是 "mempool" )的抽象, 一個內存池真實地只是一類后備緩存,它盡力一直保持一個空閑內存列表給緊急時使用。
1.1為什么要用內存池?
C++程序默認的內存管理(new,delete,malloc,free)會頻繁地在堆上分配和釋放內存,導致性能的損失,產生大量的內存碎片,降低內存的利用率。默認的內存管理因為被設計的比較通用,所以在性能上并不能做到極致。因此,很多時候需要根據業務需求設計專用內存管理器,便于針對特定數據結構和使用場合的內存管理,比如:內存池。
(1)內存碎片問題
造成堆利用率很低的一個主要原因就是內存碎片化。如果有未使用的存儲器,但是這塊存儲器不能用來滿足分配的請求,這時候就會產生內存碎片化問題。內存碎片化分為內部碎片和外部碎片。
- 內碎片:內部碎片是指一個已分配的塊比有效載荷大時發生的。(假設以前分配了10個大小的字節,現在只用了5個字節,則剩下的5個字節就會內碎片)。內部碎片的大小就是已經分配的塊的大小和他們的有效載荷之差的和。因此內部碎片取決于以前請求內存的模式和分配器實現(對齊的規則)的模式。
- 外碎片:假設系統依次分配了16byte、8byte、16byte、4byte,還剩余8byte未分配。這時要分配一個24byte的空間,操作系統回收了一個上面的兩個16byte,總的剩余空間有40byte,但是卻不能分配出一個連續24byte的空間,這就是外碎片問題。
圖片
(2)申請效率問題
例如:我們上學家里給生活費一樣,假設一學期的生活費是6000塊。
- 方式1:開學時6000塊直接給你,自己保管,自己分配如何花。
- 方式2:每次要花錢時,聯系父母,父母轉錢。
同樣是6000塊錢,第一種方式的效率肯定更高,因為第二種方式跟父母的溝通交互成本太高了。
同樣的道理,程序就像是上學的我們,操作系統就像父母,頻繁申請內存的場景下,每次需要內存,都像系統申請效率必然有影響。
1.2內存池原理
內存池的思想是,在真正使用內存之前,預先申請分配一定數量、大小預設的內存塊留作備用。當有新的內存需求時,就從內存池中分出一部分內存塊,若內存塊不夠再繼續申請新的內存,當內存釋放后就回歸到內存塊留作后續的復用,使得內存使用效率得到提升,一般也不會產生不可控制的內存碎片。
內存池設計算法原理:
- 預申請一個內存區chunk,將內存中按照對象大小劃分成多個內存塊block
- 維持一個空閑內存塊鏈表,通過指針相連,標記頭指針為第一個空閑塊
- 每次新申請一個對象的空間,則將該內存塊從空閑鏈表中去除,更新空閑鏈表頭指針
- 每次釋放一個對象的空間,則重新將該內存塊加到空閑鏈表頭
- 如果一個內存區占滿了,則新開辟一個內存區,維持一個內存區的鏈表,同指針相連,頭指針指向最新的內存區,新的內存塊從該區內重新劃分和申請
通用內存分配和釋放的缺點如下:
- 使用malloc/new申請分配堆內存時系統需要根據最先匹配、最優匹配或其它算法在內存空閑塊表中查找一塊空閑內存;使用free/delete釋放堆內存時,系統可能需要合并空閑內存塊,因此會產生額外開銷。
- 頻繁使用時會產生大量內存碎片,從而降低程序運行效率。
- 造成內存泄漏。
內存池(Memory Pool)是代替直接調用malloc/free、new/delete進行內存管理的常用方法,當申請內存空間時,會從內存池中查找合適的內存塊,而不是直接向操作系統申請。
內存池技術的優點如下:
- 堆內存碎片很少。
- 內存申請/釋放比malloc/new方式快。
- 檢查任何一個指針是否在內存池中。
- 寫一個堆轉儲(Heap-Dump)到硬盤。
- 內存泄漏檢測(memory-leak detection),當沒有釋放分配的內存時,內存池(Memory Pool)會拋出一個斷言(assertion)。
內存池可以分為不定長內存池和定長內存池兩類。不定長內存池的典型實現包括Apache Portable Runtime中的apr_pool和GNU lib C中的obstack,而定長內存池的實現則有boost_pool等。對于不定長內存池,不需要為不同的數據類型創建不同的內存池,其缺點是無法將分配出的內存回收到池內;對于定長內存池,在使用完畢后,可以將內存歸還到內存池中,但需要為不同類型的數據結構創建不同的內存池,需要內存的時候要從相應的內存池中申請內存。
二、C++內存碎片與低效
在 C++ 的世界里,內存管理是一項至關重要卻又充滿挑戰的任務。當我們編寫 C++ 程序時,常常會用到new和delete(或者malloc和free )來進行動態內存的分配與釋放。例如,當我們需要創建一個對象或者數組時,會使用new來獲取所需的內存空間,當使用完畢后,再用delete將其釋放。
int* ptr = new int;
*ptr = 10;
// 使用ptr
delete ptr;
然而,這種看似簡單直接的內存管理方式,在面對頻繁的內存分配與釋放操作時,卻會暴露出諸多問題 ,其中最為突出的便是內存碎片和效率低下的問題。
2.1內存碎片問題
隨著程序的運行,如果不斷地進行小塊內存的分配與釋放,就會導致內存空間變得越來越零散。就好比我們有一個大書架(內存空間),一開始所有的書(數據)都整齊擺放,空間充足。但隨著不斷地借書(分配內存)和還書(釋放內存),書架上會出現許多零散的小空位(內存碎片),這些小空位無法被充分利用,即使后續有新的書要放(新的內存分配請求),也可能因為沒有足夠大的連續空位而無法放置,從而導致內存利用率降低。
這種內存碎片分為內部碎片和外部碎片。內部碎片是指分配的內存塊大于實際所需,多余的部分無法被利用;外部碎片則是由于頻繁的分配和釋放,使得空閑內存分散成許多小塊,無法滿足較大內存塊的分配需求。
2.2效率低下問題
每次使用new和delete進行內存操作時,都需要與操作系統進行交互,這涉及到系統調用等開銷,會消耗一定的時間。尤其是在需要頻繁分配和釋放內存的場景下,如游戲開發中大量創建和銷毀游戲對象,或者網絡編程中頻繁處理小數據包時,這種開銷會不斷累積,嚴重影響程序的運行效率。想象一下,你每次取一件工具都要跑到很遠的倉庫去取,用完再送回去,如此頻繁操作,工作效率必然低下。同樣,C++ 程序頻繁地向操作系統請求和歸還內存,也會使程序的執行效率大打折扣。
為了解決這些問題,內存池技術應運而生,它就像是一個高效的內存管家,能夠更加合理地管理內存,提升程序的性能和穩定性。
三、C++內存池技術詳解
3.1為什么要使用內存池
- 解決內碎片問題
- 由于向內存申請的內存塊都是比較大的,所以能夠降低外碎片問題
- 一次性向內存申請一塊大的內存慢慢使用,避免了頻繁的向內存請求內存操作,提高內存分配的效率
- 但是內碎片問題無法避免,只能盡可能的降低
3.2內存池的演變
最簡單的內存分配器,做一個鏈表指向空閑內存,分配就是取出一塊來,改寫鏈表,返回,釋放就是放回到鏈表里面,并做好歸并。注意做好標記和保護,避免二次釋放,還可以花點力氣在如何查找最適合大小的內存快的搜索上,減少內存碎片,有空你了還可以把鏈表換成伙伴算法。
- 優點: 實現簡單
- 缺點: 分配時搜索合適的內存塊效率低,釋放回歸內存后歸并消耗大,實際中不實用。
定長內存分配器,即實現一個 FreeList,每個 FreeList 用于分配固定大小的內存塊,比如用于分配 32字節對象的固定內存分配器,之類的。每個固定內存分配器里面有兩個鏈表,OpenList 用于存儲未分配的空閑對象,CloseList用于存儲已分配的內存對象,那么所謂的分配就是從 OpenList 中取出一個對象放到 CloseList 里并且返回給用戶,釋放又是從 CloseList 移回到 OpenList。分配時如果不夠,那么就需要增長 OpenList:申請一個大一點的內存塊,切割成比如 64 個相同大小的對象添加到 OpenList中。這個固定內存分配器回收的時候,統一把先前向系統申請的內存塊全部還給系統。
- 優點: 簡單粗暴,分配和釋放的效率高,解決實際中特定場景下的問題有效。
- 缺點: 功能單一,只能解決定長的內存需求,另外占著內存沒有釋放。
圖片
哈希映射的FreeList池,在定長分配器的基礎上,按照不同對象大小(8,16,32,64,128,256,512,1k…64K),構造十多個固定內存分配器,分配內存時根據要申請內存大小進行對齊然后查H表,決定到底由哪個分配器負責,分配后要在內存頭部的 header 處寫上 cookie,表示由該塊內存哪一個分配器分配的,這樣釋放時候你才能正確歸還。如果大于64K,則直接用系統的 malloc作為分配,如此以浪費內存為代價你得到了一個分配時間近似O(1)的內存分配器。這種內存池的缺點是假設某個 FreeList 如果高峰期占用了大量內存即使后面不用,也無法支援到其他內存不夠的 FreeList,達不到分配均衡的效果。
- 優點:這個本質是定長內存池的改進,分配和釋放的效率高??梢越鉀Q一定長度內的問題。
- 缺點:存在內碎片的問題,且將一塊大內存切小以后,申請大內存無法使用。多線程并發場景下,鎖競爭激烈,效率降低。
范例:sgi stl 六大組件中的空間配置器就是這種設計實現的。
圖片
了解malloc底層原理
C 標準庫函數malloc 在底層使用的是 —– 分離適配,使用這種方法,分配器維護著一個空閑鏈表數組,每個空閑鏈表被組織成某種類型的顯示/隱式鏈表。每個鏈表包含大小不同的塊,這些塊的大小是大小類的成員,當要分配一個塊時,我們確定了大小類之后,對適當的空閑鏈表做首次適配,查找一個合適的塊,如果找到,那么可選地分割它,并將剩余的部分插入到適當的空閑鏈表中。如果每找到,那就搜索下一個更大的大小類的空閑鏈表,重復直到找到一個合適的塊。如果空閑鏈表中沒有合適的塊,那么就向操作系統請求額外的堆存儲器,從這個新的堆存儲器中分配一個塊,將剩余部分放置在適當的大小類中,當釋放一個塊時,我們執行合并,并將結果放在相應的空閑鏈表中。
- malloc優點: 使用自由鏈表的數組,提高分配釋放效率;減少內存碎片,可以合并空閑的內存
- malloc缺點:為了維護隱式/顯示鏈表需要維護一些信息,空間利用率不高;在多線程的情況下,會出現線程安全的問題,如果以加鎖的方式解決,會大大降低效率。
3.3內存池的核心原理
內存池的工作原理基于一種預先分配和重復利用的策略。在程序啟動階段,內存池會一次性向操作系統申請一塊較大的連續內存空間,這就好比我們提前租下了一整棟大樓(大塊內存)。隨后,內存池會將這塊大內存按照一定的規則劃分為多個較小的內存塊,這些小內存塊就像是大樓里的一個個房間。
為了有效管理這些內存塊,內存池通常會借助一些數據結構,其中鏈表和哈希表是較為常用的。以鏈表為例,所有空閑的內存塊會通過指針相互連接,形成一個空閑內存塊鏈表。當程序有內存分配請求時,內存池就會從這個鏈表中取出一個合適的內存塊分配給程序,就像從空閑房間列表中挑選一間租出去 。當程序使用完內存并釋放時,該內存塊又會被重新插入到空閑鏈表中,等待下一次分配,如同租客退房后房間又可供新租客租用。
而哈希表則可以更快速地定位到合適大小的內存塊。通過將內存塊的大小或其他特征作為鍵值,哈希表能夠在近乎常數的時間內找到滿足需求的內存塊,大大提高了內存分配的效率,尤其適用于需要頻繁分配不同大小內存塊的場景。
3.4內存池的顯著優勢
- 減少內存碎片:內存池通過預先分配大塊內存,并在內部進行小塊內存的管理,避免了頻繁向操作系統申請和釋放小塊內存所導致的內存碎片問題。因為內存池內的內存分配和釋放都在預先分配的大塊內存范圍內進行,就像在一個獨立的小區內分配房屋,不會影響到小區外的空間布局,從而保持了內存空間的連續性和規整性,提高了內存利用率。
- 提升分配釋放效率:由于內存池的內存分配和釋放操作是在用戶態進行的,無需頻繁與操作系統內核進行交互,避免了系統調用的開銷。就像在自家倉庫取放物品,無需每次都向管理員申請,節省了溝通成本和時間。而且內存池可以采用更高效的數據結構和算法來管理內存塊,使得分配和釋放操作更加快速,能夠顯著提升程序在頻繁內存操作場景下的運行效率。
- 降低內存泄漏風險:內存池可以設計成具有自動回收機制。當程序中某些內存塊被遺忘釋放時,內存池能夠在適當的時候將這些內存塊回收,重新納入可分配的內存池中,從而降低了內存泄漏的可能性。例如,當一個對象從內存池中分配內存后,在其生命周期結束時,內存池可以自動檢測并回收該對象占用的內存,無需程序員手動釋放,減少了因人為疏忽導致的內存泄漏問題。
四、C++內存池實現方案
4.1設計問題
我們在設計內存池的實現方案時,需要考慮到以下問題:
①內存池是否可以自動增長?
如果內存池的最大空間是固定的(也就是非自動增長),那么當內存池中的內存被請求完之后,程序就無法再次從內存池請求到內存。所以需要根據程序對內存的實際使用情況來確定是否需要自動增長。
②內存池的總內存占用是否只增不減?
如果內存池是自動增長的,就涉及到了“內存池的總內存占用是否是只增不減”這個問題了。試想,程序從一個自動增長的內存池中請求了1000個大小為100KB的內存片,并在使用完之后全部歸還給了內存池,而且假設程序之后的邏輯最多之后請求10個100KB的內存片,那么該內存池中的900個100KB的內存片就一直處于閑置狀態,程序的內存占用就一直不會降下來。對內存占用大小有要求的程序需要考慮到這一點。
③內存池中內存片的大小是否固定?
如果每次從內存池中的請求的內存片的大小如果不固定,那么內存池中的每個可用內存片的大小就不一致,程序再次請求內存片的時候,內存池就需要在“匹配最佳大小的內存片”和“匹配操作時間”上作出衡量。“最佳大小的內存片”雖然可以減少內存的浪費,但可能會導致“匹配時間”變長。
④內存池是否是線程安全的?
是否允許在多個線程中同時從同一個內存池中請求和歸還內存片?這個線程安全可以由內存池來實現,也可以由使用者來保證。
⑤內存片分配出去之前和歸還到內存池之后,其中的內容是否需要被清除?
程序可能出現將內存片歸還給內存池之后,仍然使用內存片的地址指針進行內存讀寫操作,這樣就會導致不可預期的結果。將內容清零只能盡量的(也不一定能)將問題拋出來,但并不能解決任何問題,而且將內容清零會消耗一定的CPU時間。所以,最終最好還是需要由內存池的使用者來保證這種安全性。
⑥是否兼容std::allocator?
STL標準庫中的大多類都支持用戶提供一個自定義的內存分配器,默認使用的是std::allocator,如std::string:
typedef basic_string<char, char_traits<char>, allocator<char> > string;
如果我們的內存池兼容std::allocator,那么我們就可以使用我們自己的內存池來替換默認的std::allocator分配器,如:
typedef basic_string<char, char_traits<char>, MemoryPoll<char> > mystring
2.2常見內存池實現方案
(1)固定大小緩沖池
固定大小緩沖池適用于頻繁分配和釋放固定大小對象的情況。
(2)dlmalloc
dlmalloc 是一個內存分配器,由Doug Lea從1987年開始編寫,目前最新版本為2.8.3,由于其高效率等特點被廣泛使用和研究。
(3) SGI STL內存分配器
SGI STL allocator 是目前設計最優秀的 C++ 內存分配器之一,其內部free_list[16] 數組負責管理從 8 bytes到128 bytes不同大小的內存塊( chunk ),每一個內存塊都由連續的固定大?。?fixed size block )的很多 chunk 組成,并用指針鏈表連接。
(4)Loki小對象分配器
Loki 分配器使用vector管理數組,可以指定 fixed size block 的大小。free blocks分布在一個連續的大內存塊中,free chunks 可以根據使用情況自動增長和減少合適的數目,避免內存分配得過多或者過少。
(5)Boost object_pool
Boost object_pool 可以根據用戶具體應用類的大小來分配內存塊,通過維護一個free nodes的鏈表來管理??梢宰詣釉黾觧odes塊,初始32個nodes,每次增加都以兩倍數向system heap要內存塊。object_pool 管理的內存塊需要在其對象銷毀的時候才返還給 system heap 。
(6)ACE_Cached_Allocator 和 ACE_Free_List
ACE 框架中包含一個可以維護固定大小的內存塊的分配器,通過在 ACE_Cached_Allocator 中定義Free_list 鏈表來管理一個連續的大內存塊,內存塊中包含多個固定大小的未使用內存區塊( free chunk),同時使用ACE_unbounded_Set維護已使用的chuncks 。
(7)TCMalloc
Google開源項目gperftools提供了內存池實現方案。TCMalloc替換了系統的malloc,更加底層優化,性能更好。
4.3STL內存分配器
分配器(allocator))是C ++標準庫的一個組件, 主要用來處理所有給定容器(vector,list,map等)內存的分配和釋放。C ++標準庫提供了默認使用的通用分配器std::allocator,但開發者可以自定義分配器。
GNU STL除了提供默認分配器,還提供了__pool_alloc、__mt_alloc、array_allocator、malloc_allocator 內存分配器。
- __pool_alloc :SGI內存池分配器
- __mt_alloc :多線程內存池分配器
- array_allocator :全局內存分配,只分配不釋放,交給系統來釋放
- malloc_allocator :堆std::malloc和std::free進行的封裝
五、C++內存池的具體實現
5.1內存池的多樣類型
在C++編程中,內存池根據其分配內存塊的方式和特點,可以分為不同的類型每種類型都有其獨特的適用場景 。了解這些類型和場景,能夠幫助我們更精準地選擇和使用內存池,優化程序性能。
(1)固定內存池
固定內存池,正如其名,每次從內存池中分配出來的內存單元大小是固定不變的。在程序初始化時,就預先設定好內存塊的大小。例如,我們創建一個用于管理小型數據結構(如鏈表節點)的固定內存池,每個內存塊大小設為 32 字節。當程序需要創建鏈表節點時,直接從這個內存池中獲取 32 字節的內存塊,無需再向操作系統申請。
固定內存池的優點十分顯著。由于內存塊大小固定,其分配和釋放操作非常高效,就像在一個裝滿同樣大小盒子的倉庫里取放物品,無需挑選和測量,直接拿取或放回即可。而且,因為內存塊大小一致,在分配和回收過程中不會產生不同大小的內存空洞,能夠有效減少內存碎片,提高內存利用率。在游戲開發中,大量的游戲對象(如子彈、怪物等)具有相同的大小和結構,使用固定內存池來管理這些對象的內存分配,可以顯著提升游戲的性能和穩定性。
(2)可變內存池
可變內存池則具有更高的靈活性,它能夠根據實際需求分配不同大小的內存塊。當程序需要分配 10 字節的內存用于存儲小型數據,又需要分配 100 字節的內存用于存儲較大的數據結構時,可變內存池都能滿足這些不同的需求。
可變內存池適用于內存需求變化較大的場景。在網絡編程中,網絡數據包的大小是不確定的,可能是幾十字節的小數據包,也可能是數千字節的大數據包,此時可變內存池就能很好地適應這種變化,為不同大小的數據包分配合適的內存空間。然而,可變內存池的實現相對復雜,因為它需要管理不同大小的內存塊,在分配和釋放內存時需要進行更復雜的算法操作,以確保內存的高效利用和避免內存碎片,就像在一個裝滿各種不同大小物品的倉庫里找東西,需要花費更多的時間和精力去尋找和整理。
5.2案例實現分析
(1)案例分析
計劃實現一個內存池管理的類MemoryPool,它具有如下特性:
- 內存池的總大小自動增長。
- 內存池中內存片的大小固定。
- 支持線程安全。
- 在內存片被歸還之后,清除其中的內容。
- 兼容std::allocator。
因為內存池的內存片的大小是固定的,不涉及到需要匹配最合適大小的內存片,由于會頻繁的進行插入、移除的操作,但查找比較少,故選用鏈表數據結構來管理內存池中的內存片。
MemoryPool中有2個鏈表,它們都是雙向鏈表(設計成雙向鏈表主要是為了在移除指定元素時,能夠快速定位該元素的前后元素,從而在該元素被移除后,將其前后元素連接起來,保證鏈表的完整性):
- data_element_ 記錄以及分配出去的內存片。
- free_element_ 記錄未被分配出去的內存片。
①內存塊結構體(Block Structure):內存塊結構體是內存池管理的基本單元,它用來表示內存池中每一個可分配的內存塊。
struct MemoryBlock {
MemoryBlock* next; // 指向下一個內存塊的指針,用于構建鏈表
// 可以在這里添加其他元數據,如內存塊的大小標識等
};
在這個結構體中,next指針起著至關重要的作用,它將各個內存塊串聯起來,形成一個鏈表結構。通過這個鏈表,內存池可以方便地管理空閑內存塊和已分配內存塊 。比如,在空閑內存塊鏈表中,每個節點都是一個MemoryBlock結構體,通過next指針可以快速找到下一個空閑塊,當有內存分配請求時,就能迅速從鏈表中取出一個空閑塊進行分配。
②空閑內存塊鏈表(Free List):空閑內存塊鏈表是內存池管理空閑內存的核心數據結構。它是由MemoryBlock結構體組成的鏈表,鏈表中的每一個節點都代表一個可用的空閑內存塊。
class MemoryPool {
private:
MemoryBlock* freeList; // 空閑內存塊鏈表的頭指針
// 其他成員變量和函數...
};
freeList作為鏈表的頭指針,指向鏈表中的第一個空閑內存塊。當內存池初始化時,會將預先分配的內存塊逐一加入到這個鏈表中,使它們成為可用的空閑資源。在內存分配過程中,內存池會從freeList所指向的鏈表頭部取出一個內存塊分配給用戶,然后更新freeList指針,使其指向下一個空閑塊;而在內存釋放時,被釋放的內存塊會被重新插入到鏈表頭部,成為新的空閑塊,方便下次分配使用。
(2)分配與釋放流程
①內存分配流程:當程序向內存池請求分配內存時,內存池首先會檢查空閑內存塊鏈表freeList是否為空。如果鏈表不為空,說明有可用的空閑內存塊,內存池會直接從鏈表頭部取出一個內存塊返回給程序,同時更新freeList指針,使其指向下一個空閑塊。這一過程就像從貨架上取走一件商品,然后調整貨架的指示標識。
void* MemoryPool::allocate() {
if (freeList == nullptr) {
expandPool(); // 內存不足,擴展內存池
}
MemoryBlock* block = freeList;
freeList = freeList->next;
return block;
}
如果freeList為空,意味著當前內存池中沒有空閑內存塊可供分配,這時內存池就需要擴展自身的內存空間。擴展內存池的方式通常是向操作系統申請一塊新的更大的內存區域,然后將這塊新內存按照內存塊的大小進行劃分,并將新劃分出的內存塊加入到空閑內存塊鏈表中,以供后續分配使用 。
②內存釋放流程:當程序使用完內存并將其釋放回內存池時,內存池會將釋放的內存塊重新插入到空閑內存塊鏈表freeList的頭部。這樣,該內存塊就又成為了可用的空閑資源,等待下一次被分配使用。
void MemoryPool::deallocate(void* ptr) {
MemoryBlock* block = static_cast<MemoryBlock*>(ptr);
block->next = freeList;
freeList = block;
}
這個過程就像是將歸還的商品重新擺放在貨架的顯眼位置,方便下次快速取用。通過這種方式,內存池實現了內存的循環利用,大大提高了內存的使用效率。
(3)線程安全機制
在多線程環境下,內存池面臨著數據競爭和不一致的問題。多個線程同時訪問和操作內存池時,可能會出現一個線程在讀取空閑內存塊鏈表時,另一個線程對鏈表進行了修改,導致讀取的數據不準確,或者出現內存塊被重復分配、釋放等錯誤情況。
為了解決這些問題,常見的實現線程安全的方法有以下幾種:
①互斥鎖(Mutex):互斥鎖是一種常用的同步機制,它可以保證在同一時刻只有一個線程能夠訪問內存池的關鍵數據結構和操作。在 C++ 中,可以使用std::mutex來實現互斥鎖。
class MemoryPool {
private:
std::mutex mtx;
MemoryBlock* freeList;
public:
void* allocate() {
std::lock_guard<std::mutex> lock(mtx);
// 分配內存的邏輯
}
void deallocate(void* ptr) {
std::lock_guard<std::mutex> lock(mtx);
// 釋放內存的邏輯
}
};
在allocate和deallocate函數中,通過std::lock_guard<std::mutex> lock(mtx);語句創建了一個鎖對象,它會在構造時自動鎖定互斥鎖mtx,在析構時自動解鎖,從而確保了在這兩個函數執行期間,其他線程無法同時訪問內存池,避免了數據競爭。
②無鎖數據結構(Lock - Free Data Structures):無鎖數據結構利用原子操作和特殊的算法來實現多線程安全,避免了鎖帶來的開銷。例如,使用std::atomic類型來實現無鎖鏈表。通過std::atomic的原子操作,可以保證對鏈表指針的修改是原子的,不會被其他線程打斷,從而實現了多線程環境下的安全操作。但無鎖數據結構的實現較為復雜,需要對原子操作和并發編程有深入的理解 。
(4)實戰演練:代碼示例
為了更直觀地理解 C++ 內存池的實現,下面我們來看一個簡單的固定內存池的代碼示例。這個內存池專門用于分配固定大小的內存塊,通過鏈表來管理空閑內存塊 。
#include <iostream>
#include <cstdlib>
#include <cassert>
// 定義內存塊結構體
struct MemoryBlock {
MemoryBlock* next; // 指向下一個內存塊的指針
};
class MemoryPool {
public:
MemoryPool(size_t blockSize, size_t initialBlocks)
: blockSize(blockSize), initialBlocks(initialBlocks) {
// 初始化內存池
initializePool();
}
~MemoryPool() {
// 釋放內存池中的所有內存
MemoryBlock* current = poolStart;
while (current) {
MemoryBlock* next = current->next;
free(current);
current = next;
}
}
void* allocate() {
if (!freeList) {
// 空閑鏈表為空,擴展內存池
expandPool();
}
MemoryBlock* block = freeList;
freeList = freeList->next;
return block;
}
void deallocate(void* ptr) {
MemoryBlock* block = static_cast<MemoryBlock*>(ptr);
block->next = freeList;
freeList = block;
}
private:
void initializePool() {
// 申請初始內存塊并構建空閑鏈表
poolStart = static_cast<MemoryBlock*>(malloc(blockSize * initialBlocks));
assert(poolStart != nullptr);
freeList = poolStart;
MemoryBlock* current = poolStart;
for (size_t i = 1; i < initialBlocks; ++i) {
current->next = static_cast<MemoryBlock*>(reinterpret_cast<char*>(current) + blockSize);
current = current->next;
}
current->next = nullptr;
}
void expandPool() {
// 擴展內存池,每次新增10個內存塊
size_t newBlocks = 10;
MemoryBlock* newBlock = static_cast<MemoryBlock*>(malloc(blockSize * newBlocks));
assert(newBlock != nullptr);
MemoryBlock* current = newBlock;
for (size_t i = 1; i < newBlocks; ++i) {
current->next = static_cast<MemoryBlock*>(reinterpret_cast<char*>(current) + blockSize);
current = current->next;
}
current->next = freeList;
freeList = newBlock;
}
size_t blockSize; // 每個內存塊的大小
size_t initialBlocks; // 初始內存塊數量
MemoryBlock* poolStart; // 內存池起始地址
MemoryBlock* freeList; // 空閑內存塊鏈表頭指針
};
// 示例使用
int main() {
MemoryPool pool(16, 5); // 每個內存塊16字節,初始5個內存塊
void* ptr1 = pool.allocate();
void* ptr2 = pool.allocate();
pool.deallocate(ptr1);
pool.deallocate(ptr2);
return 0;
}
MemoryBlock 結構體:定義了內存塊的結構,包含一個指向下一個內存塊的指針next,用于構建鏈表。
MemoryPool 類:內存池的核心類,包含以下成員:
構造函數:MemoryPool(size_t blockSize, size_t initialBlocks),接收每個內存塊的大小blockSize和初始內存塊數量initialBlocks,并調用initializePool初始化內存池。析構函數:~MemoryPool(),釋放內存池中所有申請的內存。allocate函數:從空閑鏈表中取出一個內存塊分配給用戶。如果空閑鏈表為空,則調用expandPool擴展內存池。deallocate函數:將用戶釋放的內存塊重新插入到空閑鏈表的頭部。
- initializePool 函數:在內存池初始化時,一次性向操作系統申請blockSize * initialBlocks大小的內存,并將這些內存塊串聯成一個空閑鏈表,freeList指向鏈表的頭節點。
- expandPool 函數:當空閑鏈表為空時,調用此函數擴展內存池。每次擴展新增 10 個內存塊,將新申請的內存塊加入到空閑鏈表的頭部。
- main 函數:演示了內存池的基本使用,先分配兩個內存塊,然后釋放它們 。
通過這個示例,我們可以看到內存池如何通過預先分配內存和管理空閑鏈表,實現高效的內存分配與釋放,減少內存碎片和系統調用開銷 。