STL的記憶體分配器

2021-09-30 12:02:07 字數 3484 閱讀 5798

題記:記憶體管理一直是c/c++程式的***。關於記憶體管理的話題,大致有兩類側重點,一類是記憶體的正確使用,例如c++中new和delete應該成對出現,用raii技巧管理記憶體資源,auto_ptr等方面,很多c/c++書籍中都使用技巧的介紹。另一類是記憶體管理的實現,如linux核心的slab分配器,stl中的allocator實現,以及一些特定於某種物件的記憶體管理等。最近閱讀了一些記憶體管理實現方面的資料和原始碼,整理了一下,彙編成乙個系列介紹一些常用的記憶體管理策略。

1. stl容器簡介

stl提供了很多泛型容器,如vector,list和map。程式設計師在使用這些容器時只需關心何時往容器內塞物件,而不用關心如何管理記憶體,需要用多少記憶體,這些stl容器極大地方便了c++程式的編寫。例如可以通過以下語句建立乙個vector,它實際上是乙個按需增長的動態陣列,其每個元素的型別為int整型:

stl::vectorarray;

擁有這樣乙個動態陣列後,使用者只需要呼叫push_back方法往裡面新增物件,而不需要考慮需要多少記憶體:

array.push_back(10);

array.push_back(2);

vector會根據需要自動增長記憶體,在array退出其作用域時也會自動銷毀占有的記憶體,這些對於使用者來說是透明的,stl容器巧妙的避開了繁瑣且易出錯的記憶體管理工作。

2. stl的預設記憶體分配器

隱藏在這些容器後的記憶體管理工作是通過stl提供的乙個預設的allocator實現的。當然,使用者也可以定製自己的allocator,只要實現allocator模板所定義的介面方法即可,然後通過將自定義的allocator作為模板引數傳遞給stl容器,建立乙個使用自定義allocator的stl容器物件,如:

stl::vectorarray;

大多數情況下,stl預設的allocator就已經足夠了。這個allocator是乙個由兩級分配器構成的記憶體管理器,當申請的記憶體大小大於128byte時,就啟動第一級分配器通過malloc直接向系統的堆空間分配,如果申請的記憶體大小小於128byte時,就啟動第二級分配器,從乙個預先分配好的記憶體池中取一塊記憶體交付給使用者,這個記憶體池由16個不同大小(8的倍數,8~128byte)的空閒列表組成,allocator會根據申請記憶體的大小(將這個大小round up成8的倍數)從對應的空閒塊列表取表頭塊給使用者。

這種做法有兩個優點:

1)小物件的快速分配。小物件是從記憶體池分配的,這個記憶體池是系統呼叫一次malloc分配一塊足夠大的區域給程式備用,當記憶體池耗盡時再向系統申請一塊新的區域,整個過程類似於批發和零售,起先是由allocator向總經商批發一定量的貨物,然後零售給使用者,與每次都總經商要乙個貨物再零售給使用者的過程相比,顯然是快捷了。當然,這裡的乙個問題時,記憶體池會帶來一些記憶體的浪費,比如當只需分配乙個小物件時,為了這個小物件可能要申請一大塊的記憶體池,但這個浪費還是值得的,況且這種情況在實際應用中也並不多見。

2)避免了記憶體碎片的生成。程式中的小物件的分配極易造成記憶體碎片,給作業系統的記憶體管理帶來了很大壓力,系統中碎片的增多不但會影響記憶體分配的速度,而且會極大地降低記憶體的利用率。以記憶體池組織小物件的記憶體,從系統的角度看,只是一大塊記憶體池,看不到小物件記憶體的分配和釋放。

實現時,allocator需要維護乙個儲存16個空閒塊列表表頭的陣列free_list,陣列元素i是乙個指向塊大小為8*(i+1)位元組的空閒塊列表的表頭,乙個指向記憶體池起始位址的指標start_free和乙個指向結束位址的指標end_free。空閒塊列表節點的結構如下:

union obj ;

這個結構可以看做是從乙個記憶體塊中摳出4個位元組大小來,當這個記憶體塊空閒時,它儲存了下個空閒塊,當這個記憶體塊交付給使用者時,它儲存的時使用者的資料。因此,allocator中的空閒塊鍊錶可以表示成:

obj* free_list[16];

3. 分配演算法allocator分配記憶體的演算法如下:

演算法:allocate

輸入:申請記憶體的大小size

if(size大於128)

else else else if(記憶體池中不夠分配nobj個記憶體塊,但至少可以分配乙個) else { //記憶體池連一塊記憶體塊都分配不了

先將記憶體池的記憶體塊鏈入到對應的free_list中後;

if(分配不成功) {

if(16個空閒列表中尚有空閒塊)

嘗試將16個空閒列表中空閒塊**到記憶體池中再呼叫chunk_alloc(size, nobj);

else {

呼叫第一級分配器嘗試out of memory機制是否還有用;

更新end_free為start_free+total_bytes,heap_size為2倍的total_bytes;

呼叫chunk_alloc(size,nobj);

演算法:deallocate

if(size大於128位元組)直接呼叫free(p)釋放;

else{

將size向上取8的倍數,並據此獲取對應的空閒列表表頭指標free_list_head;

調整free_list_head將p鏈入空閒列表塊中;

假設這樣乙個場景,free_list[2]已經指向了大小為24位元組的空閒塊鍊錶,如圖1所示,當使用者向allocator申請21位元組大小的記憶體塊時,allocaotr會首先檢查free_list[2]並將free_list[2]所指的記憶體塊分配給使用者,然後將表頭指向下乙個可用的空閒塊,如圖2所示。注意,當記憶體塊在鍊錶上是,前4個位元組是用作指向下乙個空閒塊,當分配給使用者時,它是一塊普通的記憶體區。

圖1 某時刻allocator的狀態

圖2 分配24位元組大小的記憶體塊

4. 小結

stl中的記憶體分配器實際上是基於空閒列表(free list)的分配策略,最主要的特點是通過組織16個空閒列表,對小物件的分配做了優化。

1)小物件的快速分配和釋放。當一次性預先分配好一塊固定大小的記憶體池後,對小於128位元組的小塊記憶體分配和釋放的操作只是一些基本的指標操作,相比於直接呼叫malloc/free,開銷小。

2)避免記憶體碎片的產生。零亂的記憶體碎片不僅會浪費記憶體空間,而且會給os的記憶體管理造成壓力。

3)盡可能最大化記憶體的利用率。當記憶體池尚有的空閒區域不足以分配所需的大小時,分配演算法會將其鏈入到對應的空閒列表中,然後會嘗試從空閒列表中尋找是否有合適大小的區域,

但是,這種記憶體分配器侷限於stl容器中使用,並不適合乙個通用的記憶體分配。因為它要求在釋放乙個記憶體塊時,必須提供這個記憶體塊的大小,以便確定**到哪個free list中,而stl容器是知道它所需分配的物件大小的,比如上述:

stl::vectorarray;

array是知道它需要分配的物件大小為sizeof(int)。乙個通用的記憶體分配器是不需要知道待釋放記憶體的大小的,類似於free(p)。

STL的記憶體分配器

題記 記憶體管理一直是c c 程式的 關於記憶體管理的話題,大致有兩類側重點,一類是記憶體的正確使用,例如c 中new和delete應該成對出現,用raii技巧管理記憶體資源,auto ptr等方面,很多c c 書籍中都使用技巧的介紹。另一類是記憶體管理的實現,如linux核心的slab分配器,st...

STL的記憶體分配器

分類 c c stl 2011 04 04 18 34 1689人閱讀收藏 舉報list 演算法vector linux核心 byte raii 題記 記憶體管理一直是c c 程式的 關於記憶體管理的話題,大致有兩類側重點,一類是記憶體的正確使用,例如c 中new和delete應該成對出現,用rai...

STL的記憶體分配器

題記 記憶體管理一直是c c 程式的 關於記憶體管理的話題,大致有兩類側重點,一類是記憶體的正確使用,例如c 中new和delete應該成對出現,用raii技巧管理記憶體資源,auto ptr等方面,很多c c 書籍中都使用技巧的介紹。另一類是記憶體管理的實現,如linux核心的slab分配器,st...