C++ STL記憶體配置的設計思想與關鍵原始碼分析
說明:我認為要讀懂STL中allocator部分的原始碼,並汲取它的思想,至少以下幾點知識你要了解:operator new和operator delete、handler函式以及一點模板知識。否則,下面你很可能看不大明白,補充點知識再學習STL原始碼比較好。
下面會結合關鍵原始碼分析C++ STL(SGI版本)的記憶體配置器設計思想。關鍵詞既然是“思想”,所以重點也就呼之欲出了。
1、allocator的簡短介紹
我閱讀的原始碼是SGI公司的版本,也是看起來最清楚的版本,各種命名最容易讓人看懂。allocator有人叫它空間配置器,因為空間不一定是記憶體,也可以是磁碟或其他輔助儲存介質。我說的記憶體配置就是指的allocator。
C++標準規範了allocator的一些必要介面,由各個廠家實現。SGI的版本與眾不同,也與標準規範不同,它的名稱是alloc而不是allocator,且不接受任何引數。
假設你在程式中顯示寫出allocator,不能像下面這樣寫:
vector<int, std::allocator<int> > iv; //錯誤的
必須要這樣寫才對:
vector<int, std::alloc> iv //好的
雖然SGI STL並不符合規範,但我們用起來好像很自然。這是因為我們使用時空間配置器是預設的,不需要我們自行指定。例如,STL中vector的宣告如下:
注意:下文我基本就用截圖來解釋程式碼了,因為我發現比起貼上程式碼,這樣更清晰(有顏色對比)。
2、原始碼檔案簡單介紹
STL標準規定:STL的allocator定義於<Memory>檔案中,<Memory>主要包含了一些標頭檔案,我們主要說的是兩個:
<stl_alloc.h>負責記憶體空間的配置與釋放;<stl_construct.h>負責物件內容的構造與析構
3、構造和析構工具:construct()和destroy()
先來說一下簡單的<stl_construct.h>檔案。這部分也不涉及什麼思想,只是有一個版本的destroy()應該認真看看。
(1)構造工具:construct()
construct()只有一個版本:
這裡使用了placement new表示式(定位new 表示式),它的作用是p指向的記憶體型別為T1,用value值初始化這塊記憶體。
(2)析構工具
destroy()倒是有幾個版本:
第一個版本:
這種顯示呼叫解構函式的做法,你也應該要熟悉。
第二個版本:
第二個版本可有點說法。呼叫層次是這樣的:destroy-> __destroy-> __destroy_aux,__destroy_aux最終呼叫第一個版本的destroy。這個版本的destroy接受一對迭代器作為引數,析構迭代器所指向的範圍內元素。
講解這個流程前,先簡單說一下trivial_destructor。
如果使用者不定義解構函式,而是用編譯器合成的,則說明解構函式基本沒有什麼用(但預設會被呼叫),稱之為trivial destructor。
那麼,如果一對迭代器所指向的元素都是trivial destructor的,就沒必要浪費時間對每個物件依次執行它的解構函式了,依靠編譯器的行為就好了。這樣在效率上是一種提升。這是STL allocator優化的一個點。
首先利用value_type()取得迭代器指向物件的型別,再利用__type_traits<T>判斷物件的解構函式是否為trivial_destructor。如果是__true_type就什麼都不做,否則迴圈呼叫第一版本的destroy()。
第三個版本:
這是針對迭代器為char*和wchar_t*的特化版本,看到它們的函式體為空,你應該猜到了,無須執行析構操作。
4、記憶體的申請與銷燬,std::alloc
記憶體的申請和銷燬由<stl_alloc.h>負責。SGI關於這一點的設計哲學是:
(1)向system heap要求空間。
(2)考慮多執行緒狀態。
(3)考慮記憶體不足時的應變策略。
(4)考慮過多“小型區塊”可能造成的記憶體碎片問題。
其實我最主要想說的是(3)(4)的設計策略,尤其是記憶體池的思路。
std::alloc的整體設計思想為:
SGI設計了雙層級配置器,第一級配置器直接使用malloc和free;第二級配置器視情況不同採用不同策略:當配置區塊超過128bytes時,視為“足夠大”,呼叫第一級配置器處理;當配置區塊小於128bytes時,視為“過小”,為降低額外負擔,採用memory pool(記憶體池)處理方式,不再借助於第一級配置器。
一、第一級記憶體配置器解析
第一級配置器主要函式有:allocate分配記憶體、deallocate釋放記憶體、reallocate重新分配記憶體等。
(1)allocate
直接呼叫C函式malloc,如果記憶體無法滿足需求,就呼叫oom_malloc函式。
原來,這是自己實現的handler函式啊,為什麼自己實現呢?因為它使用的並不是operator new配置的記憶體,所以無法使用C++new-handler機制。
關於這個機制,實際上能有不少東西可說呢,如果你不熟悉它的用途或自己實現的方法,我建議你看看《Effective C++》,或者看看我對《Effective C++》做的筆記。我這裡主要不是想分析語法方面的東西。
(2)deallocate
程式碼放上去就應該明白了。
(3)reallocate
這裡依然是呼叫C的realloc函式,如果呼叫失敗,就呼叫oom_realloc函式。
可以看出oom_realloc也是個handler函式。
基本上第一級記憶體配置器就解釋清楚了。這裡再提一點:SGI以malloc而非operator new來配置記憶體一方面是歷史原因,另一方面C++並未提供realloc函式。這樣造成了SGI不能直接使用C++的set_new_handler(),只能自己模擬一個。如何模擬set_new_handler,是有特定模式的。
二、第二級記憶體配置器解析
第二級記憶體配置器增加了一些機制,避免太多小額區塊造成的記憶體碎片。小額區塊帶來的不僅是記憶體碎片,配置時的額外負擔也是個大問題。額外負擔永遠無法避免,畢竟系統要靠這多出來的空間來管理記憶體,但區塊越小,額外負擔所佔的比例越大,自然越浪費。
第二級記憶體配置器的整體思想是:
(1)如果申請的區塊超過128bytes,就交給第一級記憶體配置器處理。
(2)如果申請的區塊小於等於128bytes,用記憶體池管理。
具體為:第二級記憶體配置器會將任何小額區塊的記憶體需求上調至8的倍數,並維護16個free-lists,各自管理大小為8、16、24、32、40、48、56、64、72、80、88、96、104、112、120、128位元組的小額區塊。
free-lists中節點結構如下:(我已經將這個union註釋)
注意:union的這種用法,也被稱為”柔性陣列“成員。本質上,與小端對齊這種儲存方式有關,這是一種技巧。
(1)allocate
第二級記憶體配置器__default_alloc_template的記憶體分配介面是allocate函式。
關鍵部分我已經用紅框註釋過了。FREELIST_INDEX(n)函式根據n的值返回16個free-list中合適的那個list的下標。
再看看ROUND_UP(n),這個函式我認為寫的挺巧妙的,將bytes值調整至8位元組的倍數。
理解這個函式你可以先舉幾個bytes值,看看返回值是什麼,自然就理解了。refill()函式很有用處,我放在下面再來介紹。
(2)deallocate()
首先判斷區塊大小,大於128位元組就呼叫第一級配置器,否則就根據需要回收的位元組大小,判斷出應該把它迴歸到哪個free list,然後由這個free list回收。
(3)refill()
這個函式挺重要的,所以要單獨拿出來介紹。當free list中沒有可用的區塊時,就呼叫這個函式,為該free list重新填充一部分空間。新的空間取自記憶體池(由chunk_alloc完成)。預設取得20個新區塊,如果記憶體池空間不足,獲得的新區塊數目可能小於20。
圖中兩個紅框是值得注意的兩個點。一旦從記憶體池獲得記憶體區塊後,拿出一個給呼叫者,另外的還要找到合適的free list”穿“起來。
(4)記憶體池函式chunk_alloc()
記憶體池一直用來供給free list。下面要將這個函式分開截圖說明了。
第一個分支:看看記憶體池內的容量夠不夠,夠的話直接拿走就好。
第二個分支:記憶體池不夠20個塊的容量,但是大於等於1個塊的長度,就把剩下的都給出去了。此時,記憶體池是空的。
第三個分支:如果記憶體池連1個對應的塊都不能提供了,比如需要32位元組,但只有8位元組了,這時候最好的做法是把這8個位元組連結到相應的free list利用上。不出意料,此時記憶體池也是空的。
第四個分支:記憶體池空空如也,所以記憶體池求助於執行時堆,堆也沒有那麼多空間了,於是就檢查這16個free list中有哪些塊沒用過呢,把這些補充到記憶體池。
第五個分支:沒錯,heap也無能為力了,記憶體池乾脆直接呼叫第一級配置器,因為第一級配置器有new-handler機制,或許有機會釋放其他記憶體拿來此處呼叫呢。如果可以,就成功否則丟擲bad-alloc異常。
小結:
如果別人問我STL記憶體配置的思想。我可能會這樣說:C++STL是兩級配置記憶體的,具體來說:第一級負責管理大塊記憶體,要保證有類似new-handler的機制;第二級負責管理小塊記憶體,為了更好的管理記憶體碎片,建立16個連結串列,每個連結串列“穿”著一塊一塊固定大小的記憶體,這16個連結串列(0至15)分別“穿”的記憶體是8、16、24…128倍數關係。需要記憶體時,從“合適”的連結串列取走,如果“合適”的連結串列記憶體不夠用了,從記憶體池裡拿,如果記憶體池不夠用了,從執行時heap裡拿,如果heap也溢位了,就交給第一級配置器,因為它有new-handler機制。
所以,堆上的東西用完了趕緊換回來,別讓記憶體池著急。
相關文章
- C++ STL 記憶體配置的設計思想與關鍵原始碼分析C++記憶體原始碼
- C++STL記憶體配置的設計思想與關鍵原始碼分析C++記憶體原始碼
- Mobx 原始碼與設計思想原始碼
- C++動態記憶體管理與原始碼剖析C++記憶體原始碼
- 侯捷C++ STL體系結構與原始碼剖析:關於moveable的說明C++原始碼
- Swoole 原始碼分析——記憶體模組之記憶體池原始碼記憶體
- Memcached記憶體管理原始碼分析記憶體原始碼
- GUN C++ STL中的vector的記憶體分配器C++記憶體
- SGI STL 的記憶體管理記憶體
- 【STL 原始碼剖析】淺談 STL 迭代器與 traits 程式設計技法原始碼AI程式設計
- MySQL • 原始碼分析 • 記憶體分配機制MySql原始碼記憶體
- Java記憶體模型與volatile關鍵字Java記憶體模型
- stl原始碼分析——map/multimap原始碼
- TMCache原始碼分析(一)—TMMemoryCache記憶體快取原始碼記憶體快取
- 鴻蒙輕核心原始碼分析:虛擬記憶體鴻蒙原始碼記憶體
- TMCache原始碼分析(一)---TMMemoryCache記憶體快取原始碼記憶體快取
- Java物件記憶體分配原理及原始碼分析Java物件記憶體原始碼
- 記憶體分析與記憶體洩漏定位記憶體
- 關於redis記憶體分析,記憶體優化Redis記憶體優化
- 讀Flink原始碼談設計:有效管理記憶體之道原始碼記憶體
- Android 記憶體快取框架 LruCache 的原始碼分析Android記憶體快取框架原始碼
- leveldb原始碼分析(1)--arena記憶體池的實現原始碼記憶體
- Netty原始碼解析 -- 記憶體池與PoolArenaNetty原始碼記憶體
- spark 原始碼分析之十五 -- Spark記憶體管理剖析Spark原始碼記憶體
- Java併發程式設計:JMM (Java記憶體模型) 以及與volatile關鍵字詳解Java程式設計記憶體模型
- spark 原始碼分析之二十二-- Task的記憶體管理Spark原始碼記憶體
- HikariPool原始碼(二)設計思想借鑑原始碼
- 記憶體管理原始碼 (轉)記憶體原始碼
- C++程式設計思想重點筆記(上)C++程式設計筆記
- C++程式設計思想筆記之四 (轉)C++程式設計筆記
- C++程式設計思想筆記之一 (轉)C++程式設計筆記
- C++程式設計思想筆記之三 (轉)C++程式設計筆記
- C++程式設計思想筆記之六 (轉)C++程式設計筆記
- C++程式設計思想筆記之二 (轉)C++程式設計筆記
- 記憶體回收導致關鍵業務抖動案例分析-論雲原生OS記憶體QoS保障記憶體
- Mybatis原始碼分析-整體設計(一)MyBatis原始碼
- Libev原始碼分析 -- 整體設計原始碼
- 平臺+外掛軟體設計思想原始碼說明 (轉)原始碼