STL源碼剖析---空間配置器

看過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::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、分配算法

// 算法: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;
	else
	{
		開始在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鏈入空閒列表塊中
	}
}
假設這樣一個場景,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::vector<int> array;
array是知道它需要分配的對象大小爲sizeof(int)。一個通用的內存分配器是不需要知道待釋放內存的大小的,類似於free(p)。





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章