SGI STL 的記憶體管理

發表於2016-10-19

1. 前言

在分析完 nginx 的記憶體池之後,也想了解一下 C++ 的記憶體管理,於是就很自然得想到 STL。STL 是一個重量級的作品,據說當時的出現,完全可以說得上是一個劃時代意義的作品。泛型、資料結構和演算法的分離、底耦合、高複用… 啊,廢話不多說了,再說下去讓人感覺像王婆賣瓜了。

啊,還忘了得加上兩位 STL 大師的名字來聊表我的敬意了。泛型大牛 Alexander  Stepanov 和 Meng Lee(李夢 — 讓人浮想的名字啊)。

2. SLT 記憶體的分配

以一個簡單的例子開始。

我們想知道的時候, 當 vec 宣告的時候和 push_back 的時候,是怎麼分配的。

其實對於一個標準的 STL 容器,當 Vetor<int> vec 的真實語句應該是 vetor<int, allocator<int>>vec,allocator 是一個標準的配置器,其作用就是為各個容器管理記憶體。這裡需要注意的是在 SGI STL 中,有兩個配置器:allocator(標準的) 和 alloc(自己實現的,非常經典,這篇文章的主要目的就是為了分析它)。

3. 一個標準的配置器

要寫一個配置器並不是很難,最重要的問題是如何分配和回收記憶體。下面看下一個標準(也許只能稱為典型)的配置器的實現:

注:程式碼有比較大的改動,因為主要是為了理解。

在使用的時候, 只需這樣 vector<int, SLD::allocator<int>>vec; 即可。vetor 便會自動呼叫我們的配置器分配記憶體了。要自己寫個配置器完全可以以這個類為模板。 而需要做的工作便是寫下自己的 allocate 和 deallocate 即可。

其實 SGI 的 allocator 就是這樣直接呼叫 operator new 和::operator delete 實現的,不過這樣做的話效率就很差了。

4. SGI STL 中的 alloc

4.1 SGI 中的記憶體管理

SGI STL 預設的介面卡是 alloc,所以我們在宣告一個 vector 的時候實際上是這樣的 vetor<int, alloc<int>>vec. 這個配置器寫得非常經典,下面就來慢慢分析它。

在我們敲下如下程式碼:

CSld* sld = new CSld;的時候其實幹了兩件事情:

  1. 呼叫::operator new 申請一塊記憶體(就是 malloc 了)
  2. 呼叫了 CSld::CSld();

而在 SGI 中, 其記憶體分配把這兩步獨立出了兩個函式:allocate 申請記憶體, construct 呼叫建構函式。他們分別在 <stl_alloc.h>, <stl_construct.h> 中。

SGI 的記憶體管理比上面所說的更復雜一些, 首先看一些 SGI 記憶體管理的幾個主要檔案,如下圖所示:

SGI Memory_thumb

<圖 1. SGI  記憶體管理>

在 stl_construct.h 中定義了兩個全域性函式 construct() 和 destroy() 來管理構造和析構。在 stl_allo.h 中定義了 5 個配置器, 我們現在關心的是 malloc_alloc_template(一級)和 default_alloc_template(二級)。

在 SGI 中,如果用了一級配置器,便是直接使用了malloc() 和 free() 函式,而如果使用了二級介面卡,則如果所申請的記憶體區域大於 128b,直接使用一級介面卡,否則,使用二級介面卡。而 stl_uninitialized.h 中,則定義了一下全域性函式來進行大塊記憶體的申請和複製。

是不是和 nginx 中的記憶體池很相似啊,不過複雜多了。

4.2 一級配置器:__malloc_alloc_template

上面說過, SGI STL 中, 如果申請的記憶體區域大於 128B 的時候,就會呼叫一級介面卡,而一級介面卡的呼叫也是非常簡單的, 直接用 malloc 申請記憶體,用 free 釋放記憶體。

可也看下如下的程式碼:

好了, 很簡單把,只是對 malloc,free, realloc 簡單的封裝。

4.3 二級配置器:__default_alloc_template

按上文所說的,SGI 的 __default_alloc_template 就是一個記憶體池了。

我們首先來看一下它的程式碼:

我們最關心的有三點:1. 記憶體池的建立。2. 記憶體的分配。 3. 記憶體的釋放。

4.3.1 SGI 記憶體池的結構

在分析記憶體池的建立之前我們首先需要看下 SGI 記憶體池的結構。

在__default_alloc_template 內部,維護著這樣一個結構體:

其實一個 free_list 就是一個連結串列,如下圖所示:

link_thumb

<圖 2. free_list 的連結串列表示>

 

這裡需要注意的有兩點:

一:SGI 內部其實維護著 16 個 free-list,對應管理的大小為 8,16,32……128.

二:_Obj 是一個 union 而不是 sturct, 我們知道,union 中的所有成員的引用在記憶體中的位置都是相同的。這裡我們用 union 就可以把每一個節點需要的額外的指標的負擔消除掉。

4.3.2 二級配置器的記憶體分配:allocate

比如現在我要申請一塊 30B 的空間,我要怎麼申請呢?

首先會呼叫二級配置器, 呼叫 allocate,在 allocate 函式之內, 從對應的 32B 的連結串列中拿出空間。

如果對應的連結串列空間不足,就會先用填充至 32B,然後用 refill() 沖洗填充該連結串列。

相應的程式碼如下:

下面畫了一張圖來幫助理解:

GetMemory_thumb

<圖 3. GetMemory>

4.3.3 二級配置器的記憶體釋放:allocate

有記憶體的分配,當然得要釋放了,下面就來看看是如何釋放的:

4.3.4 二級配置器的記憶體池:chunk_alloc

前面說過,在分配記憶體時候如果空間不足會呼叫_S_refill 函式,重新填充空間(ps: 如果這是第一個的話,就是建立了)。而_S_refill 最終呼叫的又是 chunk_alloc 函式從記憶體池中提取記憶體空間。

首先我們看一下它的原始碼:

區間 [_S_start_free, _S_end_free) 便是記憶體池的總空間(參考類:__default_alloc_template 的定義)。

當申請一塊記憶體時候,如果記憶體池總記憶體量充足,直接分配,不然就各有各的處理方法了。

下面舉一個例子來簡單得說明一下:

  1. 當第一次呼叫 chunk_alloc(32,10) 的時候,表示我要申請 10 塊__Obje(free_list), 每塊大小 32B,此時,記憶體池大小為 0,從堆空間申請 32*20 的大小的記憶體,把其中 32*10 大小的分給 free_list[3](參考圖 3)。
  2. 我再次申請 64*5 大小的空間,此時 free_list[7] 為 0, 它要從記憶體池提取記憶體,而此時記憶體池剩下 320B,剛好填充給 free_list[7],記憶體池此時大小為 0。
  3. 我第三次神奇一耳光 72*10 大小的空間,此時 free_list[8] 為 0,它要從記憶體池提取記憶體,此時記憶體池空間不足,再次從堆空間申請 72*20 大小的空間,分 72*10 給 free_list 用。

整一個 SGI 記憶體分配的大體流程就是這樣了。

5. 小結

SIG 的記憶體池比 nginx 中的複雜多了。簡單得分析一下 + 寫這篇文章花了我整整 3 個晚上的時間。

啊,我的青春啊。

相關文章