stl內存管理allocator(轉)
1. STL容器簡介
STL提供了很多泛型容器,如vector,list和map。程序員在使用這些容器時只需關心何時往容器內塞對象,而不用關心如何管理內存,需要用多少內存,這些STL容器極大地方便了C++程序的編寫。例如可以通過以下語句創建一個vector,它實際上是一個按需增長的動態數組,其每個元素的類型為int整型:
stl::vector<int> array;
擁有這樣一個動態數組後,用戶只需要調用push_back方法往裏面添加對象,而不需要考慮需要多少內存:
array.push_back(10); array.push_back(2);
vector會根據需要自動增長內存,在array退出其作用域時也會自動銷毀占有的內存,這些對於用戶來說是透明的,stl容器巧妙的避開了繁瑣且易出錯的內存管理工作。
2. STL的默認內存分配器
隱藏在這些容器後的內存管理工作是通過STL提供的一個默認的allocator實現的。當然,用戶也可以定制自己的allocator,只要實現allocator模板所定義的接口方法即可,然後通過將自定義的allocator作為模板參數傳遞給STL容器,創建一個使用自定義allocator的STL容器對象,如:
stl::vector<int, UserDefinedAllocator> array;
大多數情況下,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 { union obj *free_list_link; char client_data[1]; };
這個結構可以看做是從一個內存塊中摳出4個字節大小來,當這個內存塊空閑時,它存儲了下個空閑塊,當這個內存塊交付給用戶時,它存儲的時用戶的數據。因此,allocator中的空閑塊鏈表可以表示成:
obj* free_list[16];
3. 分配算法
allocator分配內存的算法如下:
算法:allocate
輸入:申請內存的大小size
輸出:若分配成功,則返回一個內存的地址,否則返回NULL
{
if(size大於128){ 啟動第一級分配器直接調用malloc分配所需的內存並返回內存地址;}
else {
將size向上round up成8的倍數並根據大小從free_list中取對應的表頭free_list_head;
if(free_list_head不為空)
{
從該列表中取下第一個空閑塊並調整free_list;
返回free_list_head;
}
else
{
調用refill算法建立空閑塊列表並返回所需的內存地址;
}
}
}
算法: refill
輸入:內存塊的大小size
輸出:建立空閑塊鏈表並返回第一個可用的內存塊地址
{
調用chunk_alloc算法分配若幹個大小為size的連續內存區域並返回起始地址chunk和成功分配的塊數nobj;
if(塊數為1)直接返回chunk;
否則
{
開始在chunk地址塊中建立free_list;
根據size取free_list中對應的表頭元素free_list_head;
將free_list_head指向chunk中偏移起始地址為size的地址處, 即free_list_head=(obj*)(chunk+size);
再將整個chunk中剩下的nobj-1個內存塊串聯起來構成一個空閑列表;
返回chunk,即chunk中第一塊空閑的內存塊;
}
}
算法:chunk_alloc
輸入:內存塊的大小size,預分配的內存塊塊數nobj(以引用傳遞)
輸出:一塊連續的內存區域的地址和該區域內可以容納的內存塊的塊數
{
計算總共所需的內存大小total_bytes;
if(內存池中足以分配,即end_free - start_free >= total_bytes)
{
則更新start_free;
返回舊的start_free;
}
else if(內存池中不夠分配nobj個內存塊,但至少可以分配一個)
{
計算可以分配的內存塊數並修改nobj;
更新start_free並返回原來的start_free;
}
else
{ //內存池連一塊內存塊都分配不了
先將內存池的內存塊鏈入到對應的free_list中後;
調用malloc操作重新分配內存池,大小為2倍的total_bytes加附加量,start_free指向返回的內存地址;
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
輸入:需要釋放的內存塊地址p和大小size {
if(size大於128字節)直接調用free(p)釋放;
else{
將size向上取8的倍數,並據此獲取對應的空閑列表表頭指針free_list_head;
調整free_list_head將p鏈入空閑列表塊中;
}
}
4. 小結
STL中的內存分配器實際上是基於空閑列表(free list)的分配策略,最主要的特點是通過組織16個空閑列表,對小對象的分配做了優化。
1)小對象的快速分配和釋放。當一次性預先分配好一塊固定大小的內存池後,對小於128字節的小塊內存分配和釋放的操作只是一些基本的指針操作,相比於直接調用malloc/free,開銷小。
2)避免內存碎片的產生。零亂的內存碎片不僅會浪費內存空間,而且會給OS的內存管理造成壓力。
3)盡可能最大化內存的利用率。當內存池尚有的空閑區域不足以分配所需的大小時,分配算法會將其鏈入到對應的空閑列表中,然後會嘗試從空閑列表中尋找是否有合適大小的區域,
但是,這種內存分配器局限於STL容器中使用,並不適合一個通用的內存分配。因為它要求在釋放一個內存塊時,必須提供這個內存塊的大小,以便確定回收到哪個free list中,而STL容器是知道它所需分配的對象大小的,比如上述:
stl::vector<int> array;
array是知道它需要分配的對象大小為sizeof(int)。一個通用的內存分配器是不需要知道待釋放內存的大小的,類似於free(p)。
stl內存管理allocator(轉)