STL原始碼剖析之空間配置器

2021-06-09 21:26:28 字數 4826 閱讀 7776

看過stl空間配置器的原始碼,總結一下:

1、stl空間配置器:主要分三個檔案實現,stl_construct.h  這裡定義了全域性函式construct()和destroy(),負責物件的構造和析構。stl_alloc.h檔案中定義了

一、二兩級配置器,彼此合作,配置器名為alloc. stl_uninitialized.h 這裡定義了一些全域性函式,用來填充(fill)或複製(copy)大塊記憶體資料,他們也都隸屬於stl標準規劃。

在stl_alloc.h中定義了兩級配置器,主要思想是申請大塊記憶體池,小塊記憶體直接從記憶體池中申請,當不夠用時再申請新的記憶體池,還有就是大塊記憶體直接申請。當申請空間大於128位元組時呼叫第一級配置器,第一級配置器沒有用operator::new和operator::delete來申請空間,而是直接呼叫malloc/free和realloc,並且實現了類似c++中new-handler的機制。所謂c++ new handler機制是,你可以要求系統在記憶體配置需求無法被滿足時,呼叫乙個指定的函式。換句話說,一旦::operator::new無法完成任務,在丟出std::bad_alloc異常狀態之前,會先呼叫由客端指定的處理例程,該處理例程通常稱為new-handler.new-handler解決記憶體做法有特定的模式。sgi第一級配置器的allocate()和realloc都是在呼叫malloc和realloc不成功後,改呼叫oom_malloc()和oom_realloc(),後兩者都有內迴圈,不斷呼叫"記憶體不足處理例程",期望在某次呼叫之後,獲得足夠的記憶體而圓滿完成任務。但如果「記憶體不足處理例程「並未被客端設定,oom_malloc()和oom_realloc便呼叫_throw_bad_alloc, 丟出bad_alloc異常資訊,或利用exit(1)硬生生中止程式。

在stl_alloc.h中定義的第二級配置器中,如果區塊夠大,超過128位元組時,就移交給第一級配置器處理。當區塊小於128位元組時,則以記憶體池管理,此法又稱為次層配置,每次配置一大塊記憶體,並維護對應的自由鍊錶(free-list)。下次若再有相同大小的記憶體需求,就直接從free-list中拔出。如果客端釋還小額區塊,就由配置器**到free-lists中,另外,配置器除了負責配置,也負責**。為了管理方便,sgi第二級配置器會主動將任何小額區塊的記憶體需求量上調至8的倍數。並維護16個free-lists,各自管理大小分別為8,16,24,32,40,48,56,64,72,80,88,96,104, 112,120,128 位元組的小額區塊。當申請小於等於128位元組時就會檢查對應的free list,如果free-list中有可用的區塊,就直接拿來,如果沒有,就準備為對應的free-list 重新填充空間。新的空間將取自記憶體池,預設取得20個新節點,如果記憶體池不足(還足以乙個以上的節點),就返回的相應的節點數.如果當記憶體池中連乙個節點大小都不夠時,就申請新的記憶體池,大小為2*total_bytes+round_up(heap_size>>4),totoal_bytes 為申請的空間大小,round_up調整為8的倍數,heap_size為當前總申請記憶體池的大小。如果申請該記憶體池成功就把原來記憶體池中剩下的空間分配給適當的free-list.萬一山窮水盡,整個system heap空間都不夠了(以至無法為記憶體池注入源頭活水),malloc()行動失敗,就會四處尋找有無"尚有未用區塊,且區塊足夠大 "之free lists.找到了就挖一塊交出,找不到就呼叫第一級配置器。第一級配置器其實也是使用malloc來配置記憶體。但它有out-of-memory處理機制(類似new-handler機制),或許有機會釋放其他的記憶體拿來此處使用。如果可以就成功,否則發出bad_alloc異常。

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。空閒塊列表節點的結構如下:

[cpp]view plain

copy

print?

union obj 

union obj

;

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

obj* free_list[16];

3、分配演算法

[cpp]view plain

copy

print?

// 演算法:allocate

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

else

}  }  // 演算法:refill

// 輸入:記憶體塊的大小size

}  // 演算法:chunk_alloc

// 輸入:記憶體塊的大小size,預分配的記憶體塊數nobj(以引用傳遞)

else

if(記憶體池不夠分配nobj個記憶體塊,但至少可以分配乙個) 

else

// 記憶體池連乙個記憶體塊都分配不了

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

呼叫chunk_alloc(size,nobj) 

}  } 

// 演算法:deallocate

// 演算法:allocate

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

else

}}// 演算法:refill

// 輸入:記憶體塊的大小size

}// 演算法:chunk_alloc

// 輸入:記憶體塊的大小size,預分配的記憶體塊數nobj(以引用傳遞)

else if(記憶體池不夠分配nobj個記憶體塊,但至少可以分配乙個)

else // 記憶體池連乙個記憶體塊都分配不了

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

呼叫chunk_alloc(size,nobj) }}

// 演算法:deallocate

}

假設這樣乙個場景,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原始碼剖析 空間配置器

看過stl空間配置器的原始碼,總結一下 1 stl空間配置器 主要分三個檔案實現,stl construct.h 這裡定義了全域性函式construct 和destroy 負責物件的構造和析構。stl alloc.h檔案中定義了 一 二兩級配置器,彼此合作,配置器名為alloc.stl uninit...

STL原始碼剖析 空間配置器

allocator是空間配置器而不是記憶體配置器,空間不一定是記憶體,也可以是磁碟或其他輔助儲存介質。但sgi stl提供的配置器配置的物件是記憶體。sgi標準的空間配置器,std alloctor sgi定義了乙個符合部分標準,名為alloctor的配置器,效率不高,只把c 的 operator ...

STL原始碼剖析 空間配置器

由於物件的建立分為分配記憶體和呼叫建構函式兩部分,stl allocator使用alloc allocate 來分配記憶體,construct 構造物件。construct 函式只有乙個泛化的版本,destroy 函式有乙個泛化的針對迭代器的版本,destroy aux 根據是否需要呼叫析構函式進行...