歡迎大家前往 "騰訊雲+社區" ,獲取更多騰訊海量技術實踐乾貨哦~ 本文由 "[amc" ](https://cloud.tencent.com/developer/user/1024461?fromSource=waitui)發表於 "雲+社區專欄" 在 C 語言的動態申請記憶體技術中,相比起 /` ...
歡迎大家前往騰訊雲+社區,獲取更多騰訊海量技術實踐乾貨哦~
本文由[amc](https://cloud.tencent.com/developer/user/1307425)發表於雲+社區專欄
在 C 語言的動態申請記憶體技術中,相比起 alloc
/free
系統調用,記憶體池(memory pool)是與現在系統中請求一大片連續的記憶體空間,然後在運行時根據實際需要分配出去的技術。使用記憶體池的優點有:
- 速度遠比
malloc
/free
快,因為減少了系統調用的次數,特別是頻繁申請/釋放記憶體塊的情況 - 避免了頻繁申請/釋放記憶體之後,系統的大量記憶體碎片
- 節省空間
分類
根據分配出去的記憶體大小,記憶體池可以分為兩類:
Fixed-size Allocation
每次分配出去的記憶體單元(稱為 unit 或者 cell)的大小為程式預先定義的值。釋放記憶體塊時,則只需要簡單地掛回記憶體池鏈表中即可。又稱為 “固定尺寸緩衝池”。
常規的做法是:將不同 unit size 的記憶體池整合在一起,以滿足不同記憶體塊大小的使用需求
Variable-size allocation
不分配固定長度,記憶體的分配只是在一大塊空閑的記憶體上滑動。優點是分配效率很高,缺點是成批地回收記憶體,因為釋放的記憶體無法直接重覆利用。
使用這種需要合理規劃每塊記憶體的管理區域,所以又叫做 “基於區域的” 記憶體管理。使用這種做法的分配器,舉例有 Apache Portable Runtime 中的 apr_pool 工具。本文不討論這種記憶體池。
原理和結構
概念和數據結構
定長記憶體池有一些基本和必要的概念,需要定義在記憶體池的結構數據中。以下命名方式使用變體的匈牙利命名法,比如 nNext
,n
表示變數類型為整形。類似地,p
表示指針。
Memory Unit
每次程式調用 MemPool_Alloc
獲取一個記憶體區域後,會獲得一塊連續的記憶體區域。管理一個這樣的記憶體區域的單元就成為記憶體單元 unit,有時也稱作 chunk。每個 unit 需要包含以下數據:
nNext
:整型數據,表示下一個可供分配的 unit 的標識號。功能請參見後問pData[]
:實際的記憶體區域,其大小在創建時由調用方指定
Memory Block
一個記憶體塊,記憶體塊中保存著一系列的記憶體單元。
這個數據結構需要包含以下基本信息:
nSize
:整型數據,表示該 block 在記憶體中的大小nFree
:整型,表示剩下有幾個 unit 未被分配nFirst
:整型,表示下一個可供分配的 unit 的標識號pNext
:指針,指向下一個 memory block
Memory Pool
一個記憶體池總的管理數據結構,換句話說,是一個記憶體池對象。
pBlock
:指針,指向第一個 memory blocknUnitSize
:整型,表示每個 unit 的尺寸nInitSize
:整型,表示第一個 block 的 unit 個數nGrowSize
:整型,表示在第一個 block 之外再繼續增加的每個 block 的 unit 個數
函數介面
作為一個記憶體池,需要實現以下一些基本的函數介面,或者說可以是對象方法:
memPoolCreate()
創建一個 memory pool,必須的參數為 unit size,可選參數為上文 memory pool 的 nInitSize
和 nGrowSize
。
memPoolDestroy()
銷毀整個 memory pool 並交還給操作系統。
memPoolAlloc()
從 memory pool 中分配一個 unit,其尺寸是預先定義的 unit size。
memPoolFree()
釋放一個指定的 unit。
工作過程
現在我們用一個 unit size 為 1024、init size 為 4(每一個 block 有 4 個 units)的 memory pool 為例,解釋一下記憶體池的工作原理。下文假設整型的寬度為 4 個位元組。
創建 memory pool
程式開始,調用並創建一個 memory pool。此時調用的函數為 memPoolCreate()
,程式會創建一個數據結構,相應的結構體成員及其取值如下:
memory pool alloc
當調用者第一次請求 memPoolAlloc()
時,記憶體池發現 block 鏈表為空,於是想系統申請記憶體,創建 memory block,並初始化如下(其中地址值為假設值):
其中 nSize = 4112 = sizeof(memPool) + nInitSize * sizeof(memUnit)
。每一個 nNext
依次加一,各指代著跟著自己的下一個 unit。最後一個 unit 的 nNext
值無意義,因此不說明其取值。
然後返回需要的 unit 中的記憶體。返回記憶體的邏輯如下:
- 記憶體池在 block 中查詢
nFree
成員 - 由於
nFree > 0
,表示有未分配的 unit,因此繼續在該 block 中查看nFirst
成員 nFirst
等於 0,表示該 block 中位置為 0 的 unit 可用。因此記憶體池可以將這個 unit 中的pData
地址返回給調用方。pData
的地址值計算方式為:pBlock + sizeof(memBlock) + nFirst * (sizeof(memUnit)) + sizeof(nNext) = 0x10010
nFree
減一- 修改
nFirst
的值,標記下一個可用的 unit。註意這裡的nFirst
切切不能簡單地加一,而是取返回給調用方的 unit 所對應的nNext
的值,也就是下圖(2)
處原來的值1
- 將
pData
的地址值返回。為便於說明,這塊區域我們標記為 CA
操作後各數據結構的狀態如下:
第二次調用 alloc
的情況類似。調用後各數據結構的狀態如下:
memory pool free
我們先看看結果:
- 首先程式會檢查 CA 的地址值,很快就會發現,地址 0x10010 位於上述第一個 block 的範圍之內(
0x10000 <= 0x10010 <= (0x10000 + 4112)
)。再計算偏移值可以很快得出其對應的nNext
標號,也就是上圖中的(2)
位置。 - 回收 unit,此時需要標記相應的成員值以標示 unit 的回收狀態。首先查看
nFirst
的值,參見上前幅圖,nFirst
的值為 3,表示位置(3)
處的 unit 是可用的。因此我們首先把(2)
處的nNext
值設置為 3,將其加回到可用 unit 的鏈表中 - 將
nFirst
的值修改為0
,也就是代表剛剛回收回來的 unit 的標號,而(2)
處的值賦值為 2,表示b(3)
的 unit
其實可以看到,上面就是一個簡單的鏈表操作。根據上面的過程,如果 CB 也釋放了的話,那麼 memory pool 的狀態則會變成這樣:
到這個時候,由於整個 block 已經完全回收了(nFree == nInitSize
),那麼根據不同的策略,可以考慮將整個 block 從記憶體中釋放掉。
block 滿
我們回到 alloc
的邏輯中,可以看到記憶體池最開始會檢查 block 的 nFree
成員。如果 nFree == 0
的時候,那麼就會在該 block 的 pNext
中去找到下一個 block,再去檢查 nFree
。如果發現 block 鏈表已經結束了,那就意味著當前所有的 block 已滿,必須創建新的 block。
在實際設計中,我們需要考慮選取合適的 init size 和 grow size 值。從上面的演算法中可以看到,如果 alloc
/free
調用非常頻繁時,第一個 block 的使用效率是非常高的。
變體或改進
- 有些簡化的版本中,可以不使用
pNext
來維護鏈表,也就是只有一個 block,並且記憶體的使用有一個明確且受控的上限值。這經常用在沒有malloc
系統調用的 RTOS 或者是一些對記憶體非常敏感的嵌入式系統中。 - 如果要用於多線程環境中,那麼 memory pool 結構體需要加上鎖
參考資料
- 《C++應用程式性能優化》 - 記憶體池 章節
- Memory Pool Basic Concepts
此文已由作者授權騰訊雲+社區發佈,更多原文請點擊
搜索關註公眾號「雲加社區」,第一時間獲取技術乾貨,關註後回覆1024 送你一份技術課程大禮包!
海量技術實踐經驗,盡在雲加社區!