stl源码剖析_STL源码剖析 阅读笔记(二)allocator
一、空間分配器 allocator
從使用上看,空間分配在任何語言的任何組件都不需要我們去過多關心,因為語言、組件的底層肯定都比較完整的做了這件事情。
從實現上看,學習 allocator 的原理在源碼學習中是首當其沖。因為沒有空間分配,則無從談起對象創建。這里說是空間分配,而不是內存分配,是因為也可以在內存之外的地方(如硬盤)分配空間。
分配器主要作用就是分配空間,根據規范,其需要實現一些接口,完成一些關于空間分配的功能。標準接口規范見附錄(一)。
本文會提到以下幾個方面:
- SGI STL 分配器介紹
- construct 和 destroy
- destroy 接收指針和迭代器的方法
- alloc 分配器
- 一層分配器
- 二層分配器 和 free-list
- 分配
- 釋放
- 補充
- 全局函數
- uninitialized_copy
- uninitialized_fill
- uninitialized_fill_n
二、SGI STL 的 alloc
SGI STL 的分配器與眾不同,也與標準規范不同,其名稱是 alloc 而非 allocator,而且不接受任何參數。具體來說,想在程序中明確使用 SGI 分配器,不能寫std::allocator<int>,而要寫成std::alloc。
即使它不符合標準規范,也不會對我們使用造成任何影響,因為通常我們都使用缺省的分配器,而不會自己指定。而 STL 的每一個容器都指定缺省分配器為 alloc。
當然了,SGI 也定義了符合部分標準、名為 allocator 的分配器,但出于其效率原因,STL 從未使用它,也不推薦程序員使用。它只是把 ::operator new 和 ::operator delete 做了一層薄薄的封裝,沒有做優化。詳細代碼見附錄(二)。
下面詳細聊聊 SGI STL 實現的 alloc 。
一般而言,C++的內存分配和釋放操作如下:
class Foo {...}; Foo * pf = new Foo; // 分配內存,構造對象 delete pf; // 析構對象,釋放內存- new 內含兩步操作:(1)調用 ::operator new 分配內存(2)調用 Foo::Foo() 構造對象內容。
- delete 內含兩步操作:(1)調用 Foo::~Foo() 析構對象(2)調用 ::operator delete 釋放內存。
為了精細分工,分配器將這兩個步驟分開做。內存分配由 alloc::allocate() 負責,內存釋放由 alloc::deallocate() 負責;對象構造由 ::construct() 負責,對象析構由 ::destroy() 負責。
// STL規定分配器 allocator 定義于 memory 中 #include <memory>// memory 中含有兩個文件 #include <stl_alloc.h> // 負責內存空間的分配和釋放,定義了 一級、二級分配器。 #include <stl_construct.h> // 負責對象內容的構造和析構,有 construct 和 destroy 方法。// memory 中還有一個文件 #include <stl_uninitialized.h> // 定義了一些全局函數,用來填充fill 或者 復制copy 大塊內存的數據 // 其中有如下方法 // un_initialized_copy() // un_initialized_fill() // un_initialized_fill_n() // 這些方法不屬于分配器的范疇,但與對象初值設置有關,對大規模元素初值設置很有幫助。 // 在效率上,最差會調用 construct,最佳會調用C的 memmove 進行內存移動。(一)construct 和 destroy
對于對象的 construct 和 destroy 可以概括如下圖所示。其源碼見附錄(三)
- construct 接收 指針p 和 初值value,會將value設置到p所指的空間上。
- destroy 可以接收 指針、迭代器。
- 基本類型指針:不做處理
- 對象類型指針:調用析構函數
- 迭代器:會判斷析構函數是否為 trivial destructor(無用的、沒必要的、無意義的析構函數)
- 是:則不做處理
- 否:調用 迭代器中每個元素的析構函數。
這里有個問題是,如何判斷是否為 trivial呢?
答案是:使用 __type_traits<T>::has_trivial_destructor() ,該函數會返回 __true_type 或 __false_type,前者代表是trivial,后者代表是有意義的。
該類的具體實現需要去研究下 traits,這里先不展開。
(二)STL alloc
<stl_alloc.h> 負責了對象構造前的空間分配和對象析構前的空間釋放,有下面幾個設計原則:
- 向 system heap 申請空間
- 考慮多線程狀態(為了將問題簡化,這里不討論多線程狀態)
- 考慮內存不足的應變措施
- 考慮過多小塊內存造成的碎片問題
C++ 的內存分配和釋放主要使用 ::operator new() 和 ::operator delete(),這兩個相當于C的 malloc() 和 free(),SGI 正是以 malloc 和 free 完成的內存分配和釋放。
SGI 設計了雙層分配器,如下圖所示:
- 第一級直接使用 malloc 和 free
- 第二級,當需求大于128 bytes 時,調用一級分配器;小于等于128 bytes 則調用二級分配器。
具體采用哪種分配器,需要看 __USE_MALLOC 是否被定義。定義了則用一級分配器,否則調用二級分配器。
SGI 為 alloc 提供了一個 simple_alloc 的接口封裝,使得外層使用時無需考慮內部具體用的一級還是二級。SGI STL 的容器都使用這個 simple_alloc 接口,而非直接使用 alloc。代碼見附錄(四)。
一級分配器的原理比較簡單,正常情況就是調用 malloc 和 free 做分配和釋放。當內存不夠時需要使用 oom_malloc,在該函數中,會循環調用一個 handler 來處理內存不足的情況。這個 handler 是需要自己指定的,如果沒有指定,則拋出 std::bad_alloc 異常。這個 handler 一般稱為 new-handler,在 《Effective C++》2e item7 中有特定的解決模式。
(三)STL 二級分配器
下面著重說說二級分配器。
二級分配器可以避免產生過多的小區塊,可以解決內存碎片和過多的額外開銷(系統需要多出來的空間管理內存,可以說是給系統“交稅”)。
二級分配器以內存池(memory pool)管理小于128 bytes 的內存,稱為次層分配(sub-allocation):先分配一大塊內存,組成一個自由鏈表(free-list),每次要取一定量內存時,從 free-list 中取;在用完后,分配器就歸還給 free-list。
分配器會維護 空間為 8、16、24、……、128 這16個 free-list,在分配小內存時,會向上取整(Round Up),尋找最近的 free-list。
free-list 節點結構是一個聯合體,該節點在free-list中時,內容是一個指向 下一個節點的指針,在客戶端使用時,是具體的數據。這樣一物二用,不會造成維護鏈表指針的內存浪費。這個技巧在強類型語言(Strong Typed)中如 Java 行不通,但在弱類型語言(Weak Typed)中如 C++十分常見。
union obj{union obj * free_list_link; char client_data[1]; // client use }free-list 的實現技巧次層分配中從 free list 分出內存的步驟 allocate 如下圖所示:
次層分配中釋放內存,往 free list 中歸還的步驟 deallocate 如下圖所示:
當 free-list 的空間用盡后,會觸發 refill 操作,重新給 free-list 補充 20個節點。refill 會調用 chunk_alloc,該函數中會做具體從內存池中取內存的操作。其過程如下所示。
簡而言之就是,先找自己(32找32),再找親友(64找32),實在不行就求助大家(96找32)。
三、內存基本處理工具
STL 定義了五個全局函數,除了前文提到的 construct 和 destroy,還有3個用來處理大塊內存的復制和移動的 unitialized_copy、uninitialized_fill、uninitialized_fill_n 分別對應高層次的函數 copy、fill、fill_n。
unitialized_copy 函數讓內存配置與對象構造行為分開。如果目標地址指向的空間都是未初始化區域,則會直接把源區域的對象產生復制品直接放到目標地址。STL 規范中要求該函數具有原子性,要么全部構造出來,要么全部不構造。
uninitialized_fill、uninitialized_fill_n 也和 unitialized_copy 類似。
這三個函數都會判斷 對象是否為 POD(Plain Old Data,標量 or 傳統 C 結構體),POD 會具有 trivial 函數,如果是 POD 則用最有效率的方法,如果非 POD 則用最安全的方法。過程大致如下所示。
附錄
(一)標準接口規范
根據 STL 規范,allocator 必須要實現以下接口。
(二)SGI allocator 源碼
下面是 SGI 實現的 allocator 全貌
(三)construct 和 destroy 源碼
(四)simple_alloc 和 vector
總結
以上是生活随笔為你收集整理的stl源码剖析_STL源码剖析 阅读笔记(二)allocator的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2017浦发库支票信用卡优惠活动
- 下一篇: 解决:VS中进行Qt开发,编译时报错:打