C++內存管理優化:打造你的高效內存池
在 C++ 編程的世界里,頻繁地進行內存分配與釋放操作,就像是一場無休無止的 “資源爭奪戰”。每次使用 new 和 delete,不僅消耗大量的 CPU 時間在系統調用上,還極易引發內存碎片化,讓程序性能大打折扣。想象一下,一個大型游戲在關鍵時刻,因為內存管理的低效卡頓,玩家體驗瞬間崩塌;或是一個高頻交易系統,因內存分配延遲錯失良機。而今天,我們即將開啟一段神奇之旅 —— 用 C++ 代碼打造一個高性能內存池,將這些困擾一掃而空,讓程序如虎添翼,掌控內存的高效利用。
一、引言(Introduction)
1.1 內存管理困境,你中招了嗎?
在 C++ 編程的世界里,內存管理猶如一場精細的舞蹈,一步踏錯,就可能引發性能的 “滑鐵盧”。當我們使用默認的內存管理函數,如 new/delete 或 malloc/free 在堆上分配和釋放內存時,一些隱藏的問題正悄然滋生。
想象一下,你正在開發一款實時圖形渲染引擎,每一幀都需要快速地創建和銷毀大量的圖形對象。使用 new 來分配這些對象的內存,系統首先得在內部維護的內存空閑塊表中,依據特定算法查找合適的空閑內存塊。要是找到的塊比需求大,還得切割,之后更新表格,這一系列操作帶來了不小的額外開銷。就好比快遞員在一個雜亂無章的倉庫里找包裹,每次都得花費大量時間翻找、整理,效率自然高不起來。
頻繁地分配與釋放不同大小的內存塊,還會導致內存碎片問題。這些碎片如同城市中零散分布、無法利用的小塊空地,夾雜在已分配的內存之間,使得后續較大內存需求難以得到滿足。即使系統試圖合并相鄰空閑塊,但面對復雜的內存使用情況,也常常有心無力。最終,內存利用率大打折扣,程序運行起來越來越慢,就像道路被雜物堵塞,交通逐漸癱瘓。
更令人頭疼的是內存泄漏。倘若在代碼的某個角落,new 出來的內存忘記用 delete 釋放,這就好比打開水龍頭后沒關緊,水(內存)一直在流,卻沒有回收的機制。隨著程序運行,可用內存越來越少,直到系統不堪重負,甚至崩潰,辛苦構建的程序大廈可能因這一小小的 “疏忽” 轟然倒塌。
1.2 內存池登場,救星來了!
就在傳統內存管理陷入困境之時,內存池宛如一位身披鎧甲的騎士閃亮登場。內存池,簡單來說,是在程序啟動之初,就預先申請一塊較大的連續內存區域,就好比提前圈出一大片專屬的 “內存領地”。這片領地被精心劃分成一個個大小相等(通常情況下)的小內存塊,猶如整齊排列的儲物格。
當程序運行過程中需要分配內存時,直接從這片內存池中 “提貨”,也就是取出合適的內存塊交付使用,而無需頻繁向操作系統 “求情” 索要內存。就像超市有自己的倉庫提前備好貨物,顧客來買東西時直接從倉庫取,比每次缺貨都臨時向供應商進貨快多了。如此一來,省去了系統在內部空閑塊表中反復查找、切割、更新的繁瑣流程,內存分配速度大幅提升。
由于內存池分配的是預先劃分好的固定大小塊,避免了像傳統方式那樣隨意切割內存造成的碎片,內存空間得以規整排列,利用率大大提高。而且,內存池通常設有回收機制,當某個內存塊使用完畢,會被精準地歸還到池中,等待下次復用,就像圖書館的書被借走看完后又還回書架,而不是被隨意丟棄。這一機制有效杜絕了因忘記釋放內存而導致的泄漏問題,讓程序的內存管理更加穩健、可靠,為長時間穩定運行保駕護航。
二、內存池技術詳解
2.1 什么是內存池
內存池(Memory Pool),也被稱為對象池(Object Pool),是一種內存管理策略。在這種策略中,內存被劃分為固定大小的塊,這些塊被組織在一起并被稱為“池”。當程序需要分配內存時,它會從內存池中獲取一個塊,而不是直接從操作系統請求內存。
內存池的主要優點是它可以減少內存分配和釋放的開銷。因為內存池中的塊是預先分配的,所以分配內存只需要從池中取出一個塊,而不需要進行系統調用。同樣,釋放內存只需要將塊放回池中,而不需要通知操作系統。這種方式可以大大提高內存分配和釋放的速度。
此外,內存池還可以減少內存碎片化。因為所有的塊都是相同大小的,所以它們可以緊密地排列在一起,而不會留下無法使用的空隙。這種方式可以提高內存的利用率,特別是對于那些需要大量小塊內存的程序。然而,內存池并不是萬能的。它也有一些缺點,比如它不能很好地處理大塊內存的分配,因為大塊內存的分配可能會導致內存池中的空間被浪費。此外,內存池的管理也會帶來一些開銷,特別是當內存池需要擴展或收縮時。
總的來說,內存池是一種強大的工具,它可以幫助我們更有效地管理內存。但是,像所有工具一樣,我們需要理解它的優點和缺點,以便在適當的情況下使用它。
圖片
2.2 為什么需要內存池
⑴內存碎片問題
造成堆利用率很低的一個主要原因就是內存碎片化。如果有未使用的存儲器,但是這塊存儲器不能用來滿足分配的請求,這時候就會產生內存碎片化問題。內存碎片化分為內部碎片和外部碎片。
內部碎片:內部碎片是指一個已分配的塊比有效載荷大時發生的。(假設以前分配了10個大小的字節,現在只用了5個字節,則剩下的5個字節就會內碎片)。內部碎片的大小就是已經分配的塊的大小和他們的有效載荷之差的和。因此內部碎片取決于以前請求內存的模式和分配器實現(對齊的規則)的模式。
外部碎片:假設系統依次分配了16byte、8byte、16byte、4byte,還剩余8byte未分配。這時要分配一個24byte的空間,操作系統回收了一個上面的兩個16byte,總的剩余空間有40byte,但是卻不能分配出一個連續24byte的空間,這就是外碎片問題。
圖片
⑵申請效率問題
例如:我們上學家里給生活費一樣,假設一學期的生活費是6000塊。
- 方式1:開學時6000塊直接給你,自己保管,自己分配如何花。
- 方式2:每次要花錢時,聯系父母,父母轉錢。
同樣是6000塊錢,第一種方式的效率肯定更高,因為第二種方式跟父母的溝通交互成本太高了。
同樣的道理,程序就像是上學的我們,操作系統就像父母,頻繁申請內存的場景下,每次需要內存,都像系統申請效率必然有影響。
三、C++內存管理機制
3.1 C++內存分配與釋放
在C++中,內存的分配和釋放是一個非常重要的環節。理解這個過程,對于我們深入理解內存池的設計與實現,有著至關重要的作用。
首先,我們來看一下C++中的內存分配。在C++中,我們通常使用new操作符來分配內存。當我們寫下如下代碼:
int* p = new int;
這行代碼做了什么呢?首先,new操作符會向操作系統請求一塊內存,大小為一個int的大小。如果請求成功,操作系統會返回這塊內存的地址,然后new操作符會將這個地址賦值給指針p。這樣,我們就成功地在堆(Heap)上分配了一塊內存。
那么,這個過程中有什么需要我們注意的地方呢?首先,我們需要注意的是,new操作符的執行效率。因為new操作符需要向操作系統請求內存,這個過程涉及到了系統調用,是一個相對耗時的操作。因此,如果我們在程序中頻繁地使用new操作符,可能會導致程序的執行效率降低。
其次,我們需要注意的是,new操作符可能會失敗。當操作系統的可用內存不足時,new操作符會返回一個空指針。因此,我們在使用new操作符時,需要檢查其返回值,以防止內存分配失敗。
那么,我們如何釋放內存呢?在C++中,我們使用delete操作符來釋放內存。當我們寫下如下代碼:
delete p;
這行代碼做了什么呢?delete操作符會將p指向的內存塊返回給操作系統,這樣,這塊內存就可以被其他程序使用了。同時,為了防止產生野指針,delete操作符會將p的值設置為nullptr。
在使用delete操作符時,我們需要注意的是,必須確保要刪除的指針是由new操作符分配的。如果我們試圖刪除一個非法的指針,或者是一個已經被刪除的指針,都會導致未定義的行為。此外,我們還需要注意,如果我們使用new[]操作符分配了一個數組,那么在刪除這個數組時,必須使用delete[]操作符,而不能使用delete操作符。
總的來說,C++中的內存分配和釋放是一個相對復雜的過程,需要我們仔細處理。在接下來的章節中,我們將看到,內存池可以幫助我們簡化這個過程,提高程序的執行效率,同時也可以幫助我們更好地管理內存,防止內存泄漏和野指針的產生。
3.2 C++內存管理的問題
雖然C++提供了new和delete操作符來幫助我們管理內存,但在實際使用中,我們仍然會遇到一些問題。這些問題主要包括:
- 內存碎片(Memory Fragmentation):在C++中,頻繁地進行內存的分配和釋放,會導致內存碎片的產生。內存碎片是指一些小的、無法被有效利用的內存塊。這些內存塊雖然無法被有效利用,但仍然會占用系統資源,降低系統的性能。
- 內存泄漏(Memory Leak):在C++中,如果我們分配了內存,但忘記釋放,就會導致內存泄漏。內存泄漏會導致系統的可用內存不斷減少,嚴重時甚至可能導致系統崩潰。
- 野指針(Dangling Pointer):在C++中,如果我們釋放了一塊內存,但仍然有指針指向這塊內存,這個指針就成為了野指針。野指針是非常危險的,因為我們無法預測對野指針的操作會有什么后果。
- 分配和釋放內存的效率問題:在C++中,分配和釋放內存需要調用操作系統的函數,這是一個相對耗時的操作。如果我們在程序中頻繁地分配和釋放內存,可能會導致程序的執行效率降低。
以上就是在C++中進行內存管理時可能會遇到的一些問題。在接下來的章節中,我們將看到,內存池可以幫助我們解決這些問題,提高程序的執行效率,同時也可以幫助我們更好地管理內存,防止內存泄漏和野指針的產生。
3.3 內存池解決的問題
內存池(Memory Pool)是一種內存管理策略。通過預先在內存中分配一大塊連續的內存空間,然后將這塊內存空間劃分為大小相等的小塊,當程序需要分配內存時,直接從內存池中分配一塊小內存,而不是直接向操作系統申請。這種方式可以有效地解決C++內存管理中的一些問題。
- 解決內存碎片問題:因為內存池中的內存塊大小是固定的,所以不會出現因為頻繁分配和釋放不同大小的內存導致的內存碎片問題。
- 提高內存分配效率:內存池在程序啟動時就已經預先分配了一大塊內存,所以在程序運行過程中分配和釋放內存的速度會比直接使用new和delete操作符快很多。
- 防止內存泄漏和野指針:內存池通常會提供一些機制來跟蹤內存的使用情況,比如引用計數等,這可以幫助我們更好地管理內存,防止內存泄漏和野指針的產生。
四、內存池的設計
在C/C++中我們通常使用malloc,free或new,delete來動態分配內存。一方面,因為這些函數涉及到了系統調用,所以頻繁的調用必然會導致程序性能的損耗;另一方面,頻繁的分配和釋放小塊內存會導致大量的內存碎片的產生,當碎片積累到一定的量之后,將無法分配到連續的內存空間,系統不得不進行碎片整理來滿足分配到連續的空間,這樣不僅會導致系統性能損耗,而且會導致程序對內存的利用率低下。
當然,如果我們的程序不需要頻繁的分配和釋放小塊內存,那就沒有使用內存池的必要,直接使用malloc,free或new,delete函數即可。
- malloc優點:使用自由鏈表的數組,提高分配釋放效率;減少內存碎片,可以合并空閑的內存
- malloc缺點:為了維護隱式/顯示鏈表需要維護一些信息,空間利用率不高;在多線程的情況下,會出現線程安全的問題,如果以加鎖的方式解決,會大大降低效率。
4.1 為什么要使用內存池
- 解決內碎片問題
- 由于向內存申請的內存塊都是比較大的,所以能夠降低外碎片問題
- 一次性向內存申請一塊大的內存慢慢使用,避免了頻繁的向內存請求內存操作,提高內存分配的效率
- 但是內碎片問題無法避免,只能盡可能的降低
4.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,達不到分配均衡的效果。
- 優點:這個本質是定長內存池的改進,分配和釋放的效率高。可以解決一定長度內的問題。
- 缺點:存在內碎片的問題,且將一塊大內存切小以后,申請大內存無法使用。多線程并發場景下,鎖競爭激烈,效率降低。
范例:sgi stl 六大組件中的空間配置器就是這種設計實現的。
4.3 內存池的設計原則
內存池(Memory Pool)的設計是一個復雜且需要深入理解計算機內存管理的過程。設計一個高效的內存池,我們需要遵循以下幾個原則:
- 最小化內存分配次數:內存分配是一個開銷較大的操作,頻繁的內存分配和釋放會導致系統性能下降。因此,內存池的設計應盡可能減少內存分配次數,一種常見的做法是預先分配一大塊內存,然后在需要時從中劃分出小塊內存。
- 減少內存碎片:頻繁的內存分配和釋放會導致內存碎片,這會降低內存的利用率。內存池通過管理預先分配的內存,可以有效地減少內存碎片。
- 快速響應內存請求:內存池應能快速地響應內存請求,這需要內存池有一個高效的數據結構來管理可用的內存塊。常見的做法是使用鏈表或者樹形結構來管理內存塊。
- 靈活的內存管理:內存池應能靈活地管理內存,包括內存的分配、釋放和整理。這需要內存池有一套完整的內存管理策略。
以上就是設計內存池需要遵循的原則,接下來我們將詳細介紹如何根據這些原則來設計和實現一個內存池。
五、內存池的實現方案
內存池的實現原理大致如下:提前申請一塊大內存由內存池自己管理,并分成小片供給程序使用。程序使用完之后將內存歸還到內存池中(并沒有真正的從系統釋放),當程序再次從內存池中請求內存時,內存池將池子中的可用內存片返回給程序使用。
我們在設計內存池的實現方案時,需要考慮到以下問題:
①內存池是否可以自動增長?
如果內存池的最大空間是固定的(也就是非自動增長),那么當內存池中的內存被請求完之后,程序就無法再次從內存池請求到內存。所以需要根據程序對內存的實際使用情況來確定是否需要自動增長。
②內存池的總內存占用是否只增不減?
如果內存池是自動增長的,就涉及到了“內存池的總內存占用是否是只增不減”這個問題了。試想,程序從一個自動增長的內存池中請求了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;
5.1 內存池的基本結構
內存池的基本結構主要包括兩個部分:內存塊(Memory Block)和內存塊鏈表(Memory Block List)。下面我們將詳細介紹這兩個部分。
- 內存塊(Memory Block):內存塊是內存池中最基本的單位,每個內存塊都有一個固定的大小。內存塊的大小可以根據實際需求進行設置,但通常情況下,我們會設置多種大小的內存塊,以滿足不同的內存需求。
- 內存塊鏈表(Memory Block List):內存塊鏈表是用來管理內存塊的數據結構。每個鏈表節點代表一個內存塊,節點中包含了內存塊的地址和狀態信息。通過內存塊鏈表,我們可以快速地找到一個可用的內存塊,也可以在內存塊被釋放時,快速地將其加入到鏈表中。
在實際的設計中,我們還需要考慮到內存池的擴展性和靈活性。例如,我們可以設計一個動態的內存池,當內存池中的內存塊不足時,可以動態地增加內存塊。另外,我們也可以設計多級內存池,通過多級內存池,我們可以更好地管理不同大小的內存塊,提高內存的利用率。
以上就是內存池的基本結構,接下來我們將詳細介紹如何根據這個結構來實現一個內存池。
5.2 內存池的工作原理
⑴預分配策略
內存池的高效首先得益于其預分配策略。在程序啟動之際,內存池依據對程序運行過程中內存需求的預估,向操作系統一次性申請一塊容量較大的連續內存空間,這就好比提前為一場盛宴準備了充足的食材,集中采購顯然比賓客陸續點餐時廚師一次次臨時外出采購高效得多。
這塊內存被劃分為多個大小固定的內存塊,以適配常見的內存分配需求。比如,對于一個頻繁創建小型數據結構(如鏈表節點)的程序,內存池會劃分出一批剛好能容納單個鏈表節點的小內存塊。當程序需要創建新節點時,直接從內存池中取出一個預先備好的小內存塊,無需像使用 new 時那樣,讓操作系統在碎片化的空閑內存中艱難尋覓合適空間,再進行切割、分配等繁瑣流程,大大節省了時間開銷。
⑵內存塊管理
內存池內部精心維護著一個空閑內存塊鏈表,這是它實現高效分配與回收的關鍵 “賬本”。鏈表中的每個節點對應一個空閑內存塊,它們通過指針相互串聯,猶如一條無形的鏈條串起了內存池中的空閑資源。
當程序請求分配內存時,內存池沿著鏈表快速遍歷,找到首個可用的空閑內存塊,將其從鏈表中摘下交付給程序使用。這個過程如同從掛滿鑰匙的鑰匙環上迅速取下一把所需的鑰匙,速度極快。而當內存使用完畢,需要回收時,回收的內存塊會被精準插回空閑鏈表,通常是插在表頭位置,以便下次分配時能優先被選用,就像歸還的鑰匙總是放在最順手可取的地方,方便下次快速找到。
由于內存塊大小固定,不會出現因隨意切割導致的零碎空間,避免了內存碎片的產生。而且,回收再利用機制使得內存利用率大幅提升,曾經被用過的內存塊能迅速滿血復活,重新投入戰斗,為程序持續穩定運行提供堅實保障。
⑶多線程適配
在當今多核處理器盛行,多線程編程廣泛應用的時代,內存池也具備出色的多線程適配能力。當多個線程同時對內存池發起內存分配或回收請求時,沖突風險驟升,就像多個人同時爭搶有限的資源容易亂套。
為應對這一挑戰,內存池引入了互斥鎖等同步機制。當一個線程進入內存池執行分配或回收操作時,會先獲取互斥鎖,將內存池 “鎖住”,其他線程此時只能等待,如同排隊上公共廁所,門被占用時其他人只能在外等候。線程完成操作后,釋放互斥鎖,讓其他線程有機會進入。
有些高性能內存池還采用更精細的無鎖編程技術,利用原子操作等手段減少線程等待時間,進一步提升并發性能,確保在多線程的復雜環境下,內存池依然能有條不紊地運行,為各個線程快速、穩定地提供內存服務。
5.3 C++實現內存池的步驟
實現一個內存池需要經過以下幾個步驟:
- 預分配內存:首先,我們需要預先分配一大塊內存,這個內存的大小可以根據實際需求進行設置。預分配的內存將被劃分為多個內存塊,每個內存塊的大小也可以根據需求進行設置。
- 初始化內存塊鏈表:然后,我們需要初始化內存塊鏈表。鏈表中的每個節點代表一個內存塊,節點中包含了內存塊的地址和狀態信息。初始化內存塊鏈表的過程就是將預分配的內存劃分為多個內存塊,并將這些內存塊加入到鏈表中。
- 實現內存分配函數:內存分配函數是用來分配內存的,當有內存請求時,內存分配函數會從內存塊鏈表中找到一個可用的內存塊,并返回其地址。在這個過程中,我們需要考慮內存塊的狀態,只有狀態為可用的內存塊才能被分配。
- 實現內存釋放函數:內存釋放函數是用來釋放內存的,當有內存被釋放時,內存釋放函數會將對應的內存塊加入到內存塊鏈表中,并將其狀態設置為可用。在這個過程中,我們需要考慮內存塊的狀態,只有狀態為已分配的內存塊才能被釋放。
- 實現內存整理函數:內存整理函數是用來整理內存的,它可以將連續的可用內存塊合并為一個大的內存塊,也可以將大的內存塊劃分為多個小的內存塊。通過內存整理,我們可以提高內存的利用率,減少內存碎片。
以上就是實現內存池需要經過的步驟,每個步驟都需要深入理解內存管理的原理,才能設計出一個高效的內存池。
5.4 內存池的具體實現
計劃實現一個內存池管理的類MemoryPool,它具有如下特性:
- 內存池的總大小自動增長。
- 內存池中內存片的大小固定。
- 支持線程安全。
- 在內存片被歸還之后,清除其中的內容。
- 兼容std::allocator。
因為內存池的內存片的大小是固定的,不涉及到需要匹配最合適大小的內存片,由于會頻繁的進行插入、移除的操作,但查找比較少,故選用鏈表數據結構來管理內存池中的內存片。
MemoryPool中有2個鏈表,它們都是雙向鏈表(設計成雙向鏈表主要是為了在移除指定元素時,能夠快速定位該元素的前后元素,從而在該元素被移除后,將其前后元素連接起來,保證鏈表的完整性):
- data_element_ 記錄以及分配出去的內存片。
- free_element_ 記錄未被分配出去的內存片。
MemoryPool實現代碼
代碼中使用了std::mutex等C++11才支持的特性,所以需要編譯器最低支持C++11:
#ifndef PPX_BASE_MEMORY_POOL_H_
#define PPX_BASE_MEMORY_POOL_H_
#include <climits>
#include <cstddef>
#include <mutex>
namespace ppx {
namespace base {
template <typename T, size_t BlockSize = 4096, bool ZeroOnDeallocate = true>
class MemoryPool {
public:
/* Member types */
typedef T value_type;
typedef T* pointer;
typedef T& reference;
typedef const T* const_pointer;
typedef const T& const_reference;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
typedef std::false_type propagate_on_container_copy_assignment;
typedef std::true_type propagate_on_container_move_assignment;
typedef std::true_type propagate_on_container_swap;
template <typename U> struct rebind {
typedef MemoryPool<U> other;
};
/* Member functions */
MemoryPool() noexcept;
MemoryPool(const MemoryPool& memoryPool) noexcept;
MemoryPool(MemoryPool&& memoryPool) noexcept;
template <class U> MemoryPool(const MemoryPool<U>& memoryPool) noexcept;
~MemoryPool() noexcept;
MemoryPool& operator=(const MemoryPool& memoryPool) = delete;
MemoryPool& operator=(MemoryPool&& memoryPool) noexcept;
pointer address(reference x) const noexcept;
const_pointer address(const_reference x) const noexcept;
// Can only allocate one object at a time. n and hint are ignored
pointer allocate(size_type n = 1, const_pointer hint = 0);
void deallocate(pointer p, size_type n = 1);
size_type max_size() const noexcept;
template <class U, class... Args> void construct(U* p, Args&&... args);
template <class U> void destroy(U* p);
template <class... Args> pointer newElement(Args&&... args);
void deleteElement(pointer p);
private:
struct Element_ {
Element_* pre;
Element_* next;
};
typedef char* data_pointer;
typedef Element_ element_type;
typedef Element_* element_pointer;
element_pointer data_element_;
element_pointer free_element_;
std::recursive_mutex m_;
size_type padPointer(data_pointer p, size_type align) const noexcept;
void allocateBlock();
static_assert(BlockSize >= 2 * sizeof(element_type), "BlockSize too small.");
};
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
inline typename MemoryPool<T, BlockSize, ZeroOnDeallocate>::size_type
MemoryPool<T, BlockSize, ZeroOnDeallocate>::padPointer(data_pointer p, size_type align)
const noexcept {
uintptr_t result = reinterpret_cast<uintptr_t>(p);
return ((align - result) % align);
}
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
MemoryPool<T, BlockSize, ZeroOnDeallocate>::MemoryPool()
noexcept {
data_element_ = nullptr;
free_element_ = nullptr;
}
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
MemoryPool<T, BlockSize, ZeroOnDeallocate>::MemoryPool(const MemoryPool& memoryPool)
noexcept :
MemoryPool() {
}
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
MemoryPool<T, BlockSize, ZeroOnDeallocate>::MemoryPool(MemoryPool&& memoryPool)
noexcept {
std::lock_guard<std::recursive_mutex> lock(m_);
data_element_ = memoryPool.data_element_;
memoryPool.data_element_ = nullptr;
free_element_ = memoryPool.free_element_;
memoryPool.free_element_ = nullptr;
}
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
template<class U>
MemoryPool<T, BlockSize, ZeroOnDeallocate>::MemoryPool(const MemoryPool<U>& memoryPool)
noexcept :
MemoryPool() {
}
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
MemoryPool<T, BlockSize, ZeroOnDeallocate>&
MemoryPool<T, BlockSize, ZeroOnDeallocate>::operator=(MemoryPool&& memoryPool)
noexcept {
std::lock_guard<std::recursive_mutex> lock(m_);
if (this != &memoryPool) {
std::swap(data_element_, memoryPool.data_element_);
std::swap(free_element_, memoryPool.free_element_);
}
return *this;
}
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
MemoryPool<T, BlockSize, ZeroOnDeallocate>::~MemoryPool()
noexcept {
std::lock_guard<std::recursive_mutex> lock(m_);
element_pointer curr = data_element_;
while (curr != nullptr) {
element_pointer prev = curr->next;
operator delete(reinterpret_cast<void*>(curr));
curr = prev;
}
curr = free_element_;
while (curr != nullptr) {
element_pointer prev = curr->next;
operator delete(reinterpret_cast<void*>(curr));
curr = prev;
}
}
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
inline typename MemoryPool<T, BlockSize, ZeroOnDeallocate>::pointer
MemoryPool<T, BlockSize, ZeroOnDeallocate>::address(reference x)
const noexcept {
return &x;
}
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
inline typename MemoryPool<T, BlockSize, ZeroOnDeallocate>::const_pointer
MemoryPool<T, BlockSize, ZeroOnDeallocate>::address(const_reference x)
const noexcept {
return &x;
}
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
void
MemoryPool<T, BlockSize, ZeroOnDeallocate>::allocateBlock() {
// Allocate space for the new block and store a pointer to the previous one
data_pointer new_block = reinterpret_cast<data_pointer> (operator new(BlockSize));
element_pointer new_ele_pointer = reinterpret_cast<element_pointer>(new_block);
new_ele_pointer->pre = nullptr;
new_ele_pointer->next = nullptr;
if (data_element_) {
data_element_->pre = new_ele_pointer;
}
new_ele_pointer->next = data_element_;
data_element_ = new_ele_pointer;
}
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
inline typename MemoryPool<T, BlockSize, ZeroOnDeallocate>::pointer
MemoryPool<T, BlockSize, ZeroOnDeallocate>::allocate(size_type n, const_pointer hint) {
std::lock_guard<std::recursive_mutex> lock(m_);
if (free_element_ != nullptr) {
data_pointer body =
reinterpret_cast<data_pointer>(reinterpret_cast<data_pointer>(free_element_) + sizeof(element_type));
size_type bodyPadding = padPointer(body, alignof(element_type));
pointer result = reinterpret_cast<pointer>(reinterpret_cast<data_pointer>(body + bodyPadding));
element_pointer tmp = free_element_;
free_element_ = free_element_->next;
if (free_element_)
free_element_->pre = nullptr;
tmp->next = data_element_;
if (data_element_)
data_element_->pre = tmp;
tmp->pre = nullptr;
data_element_ = tmp;
return result;
}
else {
allocateBlock();
data_pointer body =
reinterpret_cast<data_pointer>(reinterpret_cast<data_pointer>(data_element_) + sizeof(element_type));
size_type bodyPadding = padPointer(body, alignof(element_type));
pointer result = reinterpret_cast<pointer>(reinterpret_cast<data_pointer>(body + bodyPadding));
return result;
}
}
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
inline void
MemoryPool<T, BlockSize, ZeroOnDeallocate>::deallocate(pointer p, size_type n) {
std::lock_guard<std::recursive_mutex> lock(m_);
if (p != nullptr) {
element_pointer ele_p =
reinterpret_cast<element_pointer>(reinterpret_cast<data_pointer>(p) - sizeof(element_type));
if (ZeroOnDeallocate) {
memset(reinterpret_cast<data_pointer>(p), 0, BlockSize - sizeof(element_type));
}
if (ele_p->pre) {
ele_p->pre->next = ele_p->next;
}
if (ele_p->next) {
ele_p->next->pre = ele_p->pre;
}
if (ele_p->pre == nullptr) {
data_element_ = ele_p->next;
}
ele_p->pre = nullptr;
if (free_element_) {
ele_p->next = free_element_;
free_element_->pre = ele_p;
}
else {
ele_p->next = nullptr;
}
free_element_ = ele_p;
}
}
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
inline typename MemoryPool<T, BlockSize, ZeroOnDeallocate>::size_type
MemoryPool<T, BlockSize, ZeroOnDeallocate>::max_size()
const noexcept {
size_type maxBlocks = -1 / BlockSize;
return (BlockSize - sizeof(data_pointer)) / sizeof(element_type) * maxBlocks;
}
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
template <class U, class... Args>
inline void
MemoryPool<T, BlockSize, ZeroOnDeallocate>::construct(U* p, Args&&... args) {
new (p) U(std::forward<Args>(args)...);
}
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
template <class U>
inline void
MemoryPool<T, BlockSize, ZeroOnDeallocate>::destroy(U* p) {
p->~U();
}
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
template <class... Args>
inline typename MemoryPool<T, BlockSize, ZeroOnDeallocate>::pointer
MemoryPool<T, BlockSize, ZeroOnDeallocate>::newElement(Args&&... args) {
std::lock_guard<std::recursive_mutex> lock(m_);
pointer result = allocate();
construct<value_type>(result, std::forward<Args>(args)...);
return result;
}
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
inline void
MemoryPool<T, BlockSize, ZeroOnDeallocate>::deleteElement(pointer p) {
std::lock_guard<std::recursive_mutex> lock(m_);
if (p != nullptr) {
p->~value_type();
deallocate(p);
}
}
}
}
#endif // PPX_BASE_MEMORY_POOL_H_
使用示例:
#include <iostream>
#include <thread>
using namespace std;
class Apple {
public:
Apple() {
id_ = 0;
cout << "Apple()" << endl;
}
Apple(int id) {
id_ = id;
cout << "Apple(" << id_ << ")" << endl;
}
~Apple() {
cout << "~Apple()" << endl;
}
void SetId(int id) {
id_ = id;
}
int GetId() {
return id_;
}
private:
int id_;
};
void ThreadProc(ppx::base::MemoryPool<char> *mp) {
int i = 0;
while (i++ < 100000) {
char* p0 = (char*)mp->allocate();
char* p1 = (char*)mp->allocate();
mp->deallocate(p0);
char* p2 = (char*)mp->allocate();
mp->deallocate(p1);
mp->deallocate(p2);
}
}
int main()
{
ppx::base::MemoryPool<char> mp;
int i = 0;
while (i++ < 100000) {
char* p0 = (char*)mp.allocate();
char* p1 = (char*)mp.allocate();
mp.deallocate(p0);
char* p2 = (char*)mp.allocate();
mp.deallocate(p1);
mp.deallocate(p2);
}
std::thread th0(ThreadProc, &mp);
std::thread th1(ThreadProc, &mp);
std::thread th2(ThreadProc, &mp);
th0.join();
th1.join();
th2.join();
Apple *apple = nullptr;
{
ppx::base::MemoryPool<Apple> mp2;
apple = mp2.newElement(10);
int a = apple->GetId();
apple->SetId(10);
a = apple->GetId();
mp2.deleteElement(apple);
}
apple->SetId(12);
int b = -4 % 4;
int *a = nullptr;
{
ppx::base::MemoryPool<int, 18> mp3;
a = mp3.allocate();
*a = 100;
//mp3.deallocate(a);
int *b = mp3.allocate();
*b = 200;
//mp3.deallocate(b);
mp3.deallocate(a);
mp3.deallocate(b);
int *c = mp3.allocate();
*c = 300;
}
getchar();
return 0;
}
5.5避開這些坑,讓內存池更完美
在探索內存池的征途中,也有一些暗藏的 “礁石” 需要留意,稍有不慎,就可能讓內存池這葉 “扁舟” 偏離高效運行的航道。
內存泄漏隱患是首當其沖的問題。雖然內存池本身旨在避免傳統內存管理中的泄漏風險,但倘若在內存池的實現代碼里,對空閑鏈表的操作有誤,比如在回收內存塊插入鏈表時指針賦值出錯,導致內存塊 “迷失” 在內存荒野,無法被再次分配,日積月累,就如同堤壩出現微小裂縫,最終也可能引發潰堤之災,程序可用內存被悄然蠶食。
線程同步問題宛如一團亂麻,困擾著多線程環境下的內存池應用。當多個線程高頻訪問內存池,若互斥鎖使用不當,就可能引發死鎖。例如線程 A 持有鎖 1 并等待鎖 2,而線程 B 持有鎖 2 又在等待鎖 1,二者僵持不下,內存分配與回收流程陷入停滯,整個程序如同被施了定身咒,動彈不得,嚴重影響運行效率與穩定性。
過度的內存預分配也是個容易踏入的誤區。開發者滿心想著為程序運行備足 “彈藥”,卻未精準預估內存需求,申請了一塊大得離譜的內存池。這不僅造成程序啟動初期內存占用飆升,還可能導致大量內存閑置浪費,其他急需內存的程序或系統組件只能望 “內存” 興嘆,系統整體性能因此失衡,得不償失。
為躲開這些陷阱,嚴謹的代碼審查與測試是關鍵護盾。在代碼編寫階段,對涉及內存池操作的每一行代碼都要反復推敲,確保鏈表操作、指針賦值等準確無誤;借助專業的內存檢測工具,如 Valgrind 等,像偵探一樣揪出潛在的內存泄漏點。對于多線程同步,運用成熟的線程分析工具排查死鎖風險,合理設計鎖的獲取與釋放邏輯,必要時采用更高級的無鎖編程技巧化解難題。在規劃內存池大小時,結合性能分析數據與實際業務場景,精打細算,用最小的內存開銷支撐程序的順暢運行,讓內存池在正確的軌道上全速前進,助力程序性能騰飛。
六、并發內存池項目實戰
6.1 項目介紹
我寫這個項目呢,主要是為了學習,參考的是tc_malloc,項目設計分為三層結構:
圖片
- 第一層是Thread Cache,線程緩存是每個線程獨有的,在這里設計的是用于小于64k的內存分配,線程在這里申請不需要加鎖,每一個線程都有自己獨立的cache,這也就是這個項目并發高效的地方。
- 第二層是Central Cache,在這里是所有線程共享的,它起著承上啟下的作用,Thread Cache是按需要從Central Cache中獲取對象,它就要起著平衡多個線程按需調度的作用,既可以將內存對象分配給Thread Cache來的每個線程,又可以將線程歸還回來的內存進行管理。Central Cache是存在競爭的,所以在這里取內存對象的時候是需要加鎖的,但是鎖的力度可以控制得很小。
- 第三層是Page Cache,存儲的是以頁為單位存儲及分配的,Central Cache沒有內存對象(Span)時,從Page cache分配出一定數量的Page,并切割成定長大小的小塊內存,分配給Central Cache。Page Cache會回收Central Cache滿足條件的Span(使用計數為0)對象,并且合并相鄰的頁,組成更大的頁,緩解內存碎片的問題。
注:怎么實現每個線程都擁有自己唯一的線程緩存呢?
為了避免加鎖帶來的效率,在Thread Cache中使用(tls)thread local storage保存每個線程本地的Thread Cache的指針,這樣Thread Cache在申請釋放內存是不需要鎖的。因為每一個線程都擁有了自己唯一的一個全局變量。
TLS分為靜態的和動態的:
- 靜態的TLS是:直接定義
- 動態的TLS是:調用系統的API去創建的,我們這個項目里面用到的就是靜態的TLS
6.2 設計Thread Cache
圖片
ThreadCache.h:
#pragma once
#include "Common.h"
class ThreadCache
{
private:
Freelist _freelist[NLISTS];//自由鏈表
public:
//申請和釋放內存對象
void* Allocate(size_t size);
void Deallocate(void* ptr, size_t size);
//從中心緩存獲取對象
void* FetchFromCentralCache(size_t index, size_t size);
//釋放對象時,鏈表過長時,回收內存回到中心堆
void ListTooLong(Freelist* list, size_t size);
};
//靜態的,不是所有可見
//每個線程有個自己的指針, 用(_declspec (thread)),我們在使用時,每次來都是自己的,就不用加鎖了
//每個線程都有自己的tlslist
_declspec (thread) static ThreadCache* tlslist = nullptr;
申請內存:
- 當內存申請size<=64k時在Thread Cache中申請內存,計算size在自由鏈表中的位置,如果自由鏈表中有內存對象時,直接從FistList[i]中Pop一下對象,時間復雜度是O(1),且沒有鎖競爭。
- 當FreeList[i]中沒有對象時,則批量從Central Cache中獲取一定數量的對象,插入到自由鏈表并返回一個對象。
釋放內存:
- 當釋放內存小于64k時將內存釋放回Thread Cache,計算size在自由鏈表中的位置,將對象Push到FreeList[i].
- 當鏈表的長度過長,也就是超過一次向中心緩存分配的內存塊數目時則回收一部分內存對象到Central Cache。
6.3 對齊大小的設計(對齊規則)
//專門用來計算大小位置的類
class SizeClass
{
public:
//獲取Freelist的位置
inline static size_t _Index(size_t size, size_t align)
{
size_t alignnum = 1 << align; //庫里實現的方法
return ((size + alignnum - 1) >> align) - 1;
}
inline static size_t _Roundup(size_t size, size_t align)
{
size_t alignnum = 1 << align;
return (size + alignnum - 1)&~(alignnum - 1);
}
public:
// 控制在12%左右的內碎片浪費
// [1,128] 8byte對齊 freelist[0,16)
// [129,1024] 16byte對齊 freelist[16,72)
// [1025,8*1024] 128byte對齊 freelist[72,128)
// [8*1024+1,64*1024] 1024byte對齊 freelist[128,184)
inline static size_t Index(size_t size)
{
assert(size <= MAX_BYTES);
// 每個區間有多少個鏈
static int group_array[4] = { 16, 56, 56, 56 };
if (size <= 128)
{
return _Index(size, 3);
}
else if (size <= 1024)
{
return _Index(size - 128, 4) + group_array[0];
}
else if (size <= 8192)
{
return _Index(size - 1024, 7) + group_array[0] + group_array[1];
}
else//if (size <= 65536)
{
return _Index(size - 8 * 1024, 10) + group_array[0] + group_array[1] + group_array[2];
}
}
// 對齊大小計算,向上取整
static inline size_t Roundup(size_t bytes)
{
assert(bytes <= MAX_BYTES);
if (bytes <= 128){
return _Roundup(bytes, 3);
}
else if (bytes <= 1024){
return _Roundup(bytes, 4);
}
else if (bytes <= 8192){
return _Roundup(bytes, 7);
}
else {//if (bytes <= 65536){
return _Roundup(bytes, 10);
}
}
//動態計算從中心緩存分配多少個內存對象到ThreadCache中
static size_t NumMoveSize(size_t size)
{
if (size == 0)
return 0;
int num = (int)(MAX_BYTES / size);
if (num < 2)
num = 2;
if (num > 512)
num = 512;
return num;
}
// 根據size計算中心緩存要從頁緩存獲取多大的span對象
static size_t NumMovePage(size_t size)
{
size_t num = NumMoveSize(size);
size_t npage = num*size;
npage >>= PAGE_SHIFT;
if (npage == 0)
npage = 1;
return npage;
}
};
6.4 設計Thread Cache
- Central Cache本質是由一個哈希映射的Span對象自由雙向鏈表構成
- 為了保證全局只有唯一的Central Cache,這個類被可以設計成了單例模式
- 單例模式采用餓漢模式,避免高并發下資源的競爭
圖片
CentralCache.h:
#pragma once
#include "Common.h"
//上面的ThreadCache里面沒有的話,要從中心獲取
/*
進行資源的均衡,對于ThreadCache的某個資源過剩的時候,可以回收ThreadCache內部的的內存
從而可以分配給其他的ThreadCache
只有一個中心緩存,對于所有的線程來獲取內存的時候都應該是一個中心緩存
所以對于中心緩存可以使用單例模式來進行創建中心緩存的類
對于中心緩存來說要加鎖
*/
//設計成單例模式
class CentralCache
{
public:
static CentralCache* Getinstence()
{
return &_inst;
}
//從page cache獲取一個span
Span* GetOneSpan(SpanList& spanlist, size_t byte_size);
//從中心緩存獲取一定數量的對象給threa cache
size_t FetchRangeObj(void*& start, void*& end, size_t n, size_t byte_size);
//將一定數量的對象釋放給span跨度
void ReleaseListToSpans(void* start, size_t size);
private:
SpanList _spanlist[NLISTS];
private:
CentralCache(){}//聲明不實現,防止默認構造,自己創建
CentralCache(CentralCache&) = delete;
static CentralCache _inst;
};
申請內存:
- 當Thread Cache中沒有內存時,就會批量向Central Cache申請一些內存對象,Central Cache也有一個哈希映射的freelist,freelist中掛著span,從span中取出對象給Thread Cache,這個過程是需要加鎖的。
- Central Cache中沒有非空的span時,則將空的span鏈在一起,向Page Cache申請一個span對象,span對象中是一些以頁為單位的內存,切成需要的內存大小,并鏈接起來,掛到span中。
- Central Cache的span中有一個_usecount,分配一個對象給Thread Cache,就++_usecount。
釋放內存:
- 當Thread Cache過長或者線程銷毀,則會將內存釋放回Central Cache中的,釋放回來時- -_usecount。
- 當_usecount減到0時則表示所有對象都回到了span,則將Span釋放回Page Cache,Page Cache中會對前后相鄰的空閑頁進行合并。
特別關心:什么是span?一個span是由多個頁組成的一個span對象。一頁大小是4k。
//Span是一個跨度,既可以分配內存出去,也是負責將內存回收回來到PageCache合并
//是一鏈式結構,定義為結構體就行,避免需要很多的友元
struct Span
{
PageID _pageid = 0;//頁號
size_t _npage = 0;//頁數
Span* _prev = nullptr;
Span* _next = nullptr;
void* _list = nullptr;//鏈接對象的自由鏈表,后面有對象就不為空,沒有對象就是空
size_t _objsize = 0;//對象的大小
size_t _usecount = 0;//對象使用計數,
};
特別關心:關于spanlist,設計為一個雙向鏈表,插入刪除效率較高。
//和上面的Freelist一樣,各個接口自己實現,雙向帶頭循環的Span鏈表
class SpanList
{
public:
Span* _head;
std::mutex _mutex;
public:
SpanList()
{
_head = new Span;
_head->_next = _head;
_head->_prev = _head;
}
~SpanList()//釋放鏈表的每個節點
{
Span * cur = _head->_next;
while (cur != _head)
{
Span* next = cur->_next;
delete cur;
cur = next;
}
delete _head;
_head = nullptr;
}
//防止拷貝構造和賦值構造,將其封死,沒有拷貝的必要,不然就自己會實現淺拷貝
SpanList(const SpanList&) = delete;
SpanList& operator=(const SpanList&) = delete;
//左閉右開
Span* Begin()//返回的一個數據的指針
{
return _head->_next;
}
Span* End()//最后一個的下一個指針
{
return _head;
}
bool Empty()
{
return _head->_next == _head;
}
//在pos位置的前面插入一個newspan
void Insert(Span* cur, Span* newspan)
{
Span* prev = cur->_prev;
//prev newspan cur
prev->_next = newspan;
newspan->_next = cur;
newspan->_prev = prev;
cur->_prev = newspan;
}
//刪除pos位置的節點
void Erase(Span* cur)//此處只是單純的把pos拿出來,并沒有釋放掉,后面還有用處
{
Span* prev = cur->_prev;
Span* next = cur->_next;
prev->_next = next;
next->_prev = prev;
}
//尾插
void PushBack(Span* newspan)
{
Insert(End(), newspan);
}
//頭插
void PushFront(Span* newspan)
{
Insert(Begin(), newspan);
}
//尾刪
Span* PopBack()//實際是將尾部位置的節點拿出來
{
Span* span = _head->_prev;
Erase(span);
return span;
}
//頭刪
Span* PopFront()//實際是將頭部位置節點拿出來
{
Span* span = _head->_next;
Erase(span);
return span;
}
void Lock()
{
_mutex.lock();
}
void Unlock()
{
_mutex.unlock();
}
};
特別關心:怎么才能將Thread Cache中的內存對象還給它原來的span呢?
答:可以在Page Cache中維護一個頁號到span的映射,當Span Cache給Central Cache分配一個span時,將這個映射更新到unordered_map中去,在Thread Cache還給Central Cache時,可以查這個unordered_map找到對應的span。
6.5 設計Page Cache
- Page cache是一個以頁為單位的span自由鏈表。
- 為了保證全局只有唯一的Page cache,這個類可以被設計成了單例模式。
- 本單例模式采用餓漢模式。
圖片
PageCache.h
#pragma once
#include "Common.h"
//對于Page Cache也要設置為單例,對于Central Cache獲取span的時候
//每次都是從同一個page數組中獲取span
//單例模式
class PageCache
{
public:
static PageCache* GetInstence()
{
return &_inst;
}
Span* AllocBigPageObj(size_t size);
void FreeBigPageObj(void* ptr, Span* span);
Span* _NewSpan(size_t n);
Span* NewSpan(size_t n);//獲取的是以頁為單位
//獲取從對象到span的映射
Span* MapObjectToSpan(void* obj);
//釋放空間span回到PageCache,并合并相鄰的span
void ReleaseSpanToPageCache(Span* span);
private:
SpanList _spanlist[NPAGES];
//std::map<PageID, Span*> _idspanmap;
std::unordered_map<PageID, Span*> _idspanmap;
std::mutex _mutex;
private:
PageCache(){}
PageCache(const PageCache&) = delete;
static PageCache _inst;
};
申請內存:
- 當Central Cache向page cache申請內存時,Page Cache先檢查對應位置有沒有span,如果沒有則向更大頁尋找一個span,如果找到則分裂成兩個。比如:申請的是4page,4page后面沒有掛span,則向后面尋找更大的span,假設在10page位置找到一個span,則將10page span分裂為一個4page span和一個6page span。
- 如果找到128 page都沒有合適的span,則向系統使用mmap、brk或者是VirtualAlloc等方式申請128page span掛在自由鏈表中,再重復1中的過程。
釋放內存:如果Central Cache釋放回一個span,則依次尋找span的前后_pageid的span,看是否可以合并,如果合并繼續向前尋找。這樣就可以將切小的內存合并收縮成大的span,減少內存碎片。
6.6 項目不足
項目的獨立性不足:
- 不足:當前實現的項目中我們并沒有完全脫離malloc,比如在內存池自身數據結構的管理中,如SpanList中的span等結構,我們還是使用的new Span這樣的操作,new的底層使用的是malloc,所以還不足以替換malloc,因為們本身沒有完全脫離它。
- 解決方案:項目中增加一個定長的ObjectPool的對象池,對象池的內存直接使用brk、VirarulAlloc等向系統申請,new Span替換成對象池申請內存。這樣就完全脫離的malloc,就可以替換掉malloc。
平臺及兼容性:
- linux等系統下面,需要將VirtualAlloc替換為brk等。
- x64系統下面,當前的實現支持不足。比如:id查找Span得到的映射,我們當前使用的是map<id,
Span*>。在64位系統下面,這個數據結構在性能和內存等方面都是撐不住。需要改進后基數樹。 - 具體參考:Linux內核數據結構:基數樹Radix Tree