咱們在平常編寫C++程序時,經常會用到咱們的STL標準庫來幫助咱們解決問題,這當中咱們用得最多估計就是它裏面的vector、list容器了,它們帶來的便利不用多說(畢竟OJ、刷題什麼的,基本全是它們的身影),而在平常學習中咱們對STL中另外一大組件—空間配置器 瞭解可能就相對較少了。不過它也是個有用的東西,之因此這麼說,主要就在於它解決了在內存分配過程當中出現的內存碎片問題,具體就是
git
如上,對於一塊從堆上分配的內存,因爲對該塊內存的釋放一般是不肯定的,由於取決於用戶,對於剛釋放完的那32字節,雖歸還給了os,但因爲中間都是碎片化的內存,因此此時想利用那32字節再從os申請20字節內存便沒法完成。
而在多線程環境下,這種內存碎片問題帶來的影響就更大了,多個線程頻繁的進行內存申請和釋放,同時申請、釋放的內存塊有大有小;程序執行過程中這些碎片的內存就有可能間接形成內存浪費,再一個os要對這樣頻繁的操做管理,勢必會影響到它的效率。github
STL中配置器老是隱藏在一切組間(具體地說是container)的背後,默默工做。但站在STL實現角度來看,咱們第一個須要搞清楚的就是空間配置器,由於咱們操做全部STL對象基本都會存放容器當中,而容器必定須要配置空間來置放資料的,不弄清它的原理,一定會影響之後對STL的深刻學習。
而在SGI STL中,std::alloc 爲默認的空間配置器:
例如,vector<int, std::alloc> v
是的,它的寫法好像並非標準的寫法(標準寫法應該是allocator),並且它也不接受參數!但這並不會給咱們帶來困擾,由於它是默認的,不多須要咱們自行指定配置器名稱。(至於爲何不用allocator這個更標準的寫法,這源於它的效率問題。具體能夠參考STL源碼剖析),今天主要來看看alloc版本配置器實現原理,加深咱們關於空間分配的理解。
配置器要完成的其實就是對象構造前的空間配置和對象析構後的空間釋放。參考SGI中作法配置器對此設計要考慮:windows
基於此,alloc實現中設計了雙層級配置器模型。一級配置器直接使用malloc和free,二級配置器則視狀況採起不一樣的策略,具體來說就是:當需求的內存塊超過128字節時,就將其視爲大塊內存需求,便直接調用一級配置器來分配;當須要內存塊< 128字節,便交由二級配置器來管理(這當中可能還聯合一級配置器一塊兒使用,具體緣由在後面)。數組
首先,一級配置器STL默認名一般是__malloc_alloc_template<0>
.在STL實現中將它typedef爲了alloc
。再一個值得注意的則是:源於__USE_MALLOC
一般未定義,因此一級配置器並非STL中默認的配置器。
一級配置器模擬實現:安全
#pragma once #include <iostream> #include <windows.h> using namespace std; //一級空間配置器 typedef void(*HANDLE_FUNC)(); template <int inst> // inst爲預留參數,方便之後擴展 class __MallocAllocTemplate { private: /*定義函數指針類型成員,方便回調執行用戶 自定義的內存釋放函數,該成員默認設置不執行*/ static HANDLE_FUNC __malloc_alloc_oom_handler; static void* OOM_Malloc(size_t n){ while (1){ if (0 == __malloc_alloc_oom_handler){ throw bad_alloc(); }else{ __malloc_alloc_oom_handler(); //釋放內存 Sleep(200); void* ret = malloc(n); if (ret) return ret; } } } public: static void* Allocate(size_t n){ void *result = malloc(n); //malloc申請失敗,執行OOM_Malloc再請求申請內存 if (0 == result) result = OOM_Malloc(n); cout<<"申請成功!"<<endl; return result; } static void Deallocate(void *p, size_t /* n */){ free(p); } /*設置oom_malloc句柄函數,*/ static HANDLE_FUNC SetMallocHandler(HANDLE_FUNC f){ HANDLE_FUNC old = f; __malloc_alloc_oom_handler = f; return old; } }; template<int inst> HANDLE_FUNC __MallocAllocTemplate<inst>::__malloc_alloc_oom_handler = 0; //自定義的內存釋放函數 static void FreeMemory(){ cout<<"執行用戶自定義函數,開始釋放內存..."<<endl; } void Test_Alloc1(); void Test_Alloc2();
當中的內存分配Allocate和釋放Dellocate都是簡單封裝malloc和free,同時該類的成員函數中都是用static修飾的靜態成員函數多線程
實現了一個static void* OOM_Malloc(size_t ) 函數 。這一般是在一次malloc調用失敗後,再去調用它來拋出bad_alloc異常。但這裏設計考慮它的擴展性。併發
終於實現完了一級配置器,惋惜的是咱們從前面就不難發現:這個單純封裝malloc、free的一級配置器貌似效率並不高吧~
其實,下面所述的二級配置器纔是STL中真正具備設計哲學一個做品。函數
首先,當調用方需求的內存小於128字節時,此時便要利用二級配置器來分配內存了,固然不只僅如此,這個二級配置器還要進行內存回收工做。整個空間配置器正是由於它才能達到真正的迅速分配內存。至於原因則還要從它的組成結構開始提及
它的組成結構有兩個:高併發
注意到有兩個指針startFree、endfree,它們就至關於水位線的一種東西,它表示了內存池的大小。
自由鏈表中實際上是一個大小爲16的指針數組,間隔爲8的倍數。各自管理大小分別爲8,16,24 . . . 120,128 字節的小額區塊。在每一個下標下掛着一個鏈表,把一樣大小的內存塊連接在一塊兒。(這貌似就是哈希桶吧!)
首先,當咱們的容器向配置器申請<128小塊內存時,先就要從對應的鏈表中取得一塊。具體就是:拿着申請內存大小進行近似除8的方法算得在這個指針數組中下標,緊接着就能夠從鏈表中取出第一塊內存返回。當一塊內存用完,用戶釋放時,進行一樣的操做,接着計算對於的下標再將該塊內存頭插到對應鏈表中。
(固然實際計算這些對應下標時,採用兩個更準確、高效的函數,見後面,這裏只是簡單分析)
看看鏈表結點結構和連接
二級配置器中有一個這樣結構
union Obj{ union Obj* _freelistlink; char client_data[1]; /* The client sees this. 用來調試用的*/ };
好了,咱們到這討論的還處在一個大前提上——freelist下面掛有連接起來的小區塊。當freelist上的某個位置下面沒有掛上這些小區塊呢?因此,這就是下面Refill,chunkAlloc這兩個函數要乾的事情了。
#pragma once #include "Allocator.h" /////////////////////////////////////////////////////////////////////// //二級空間配置器 template <bool threads, int inst> class __DefaultAllocTemplate { public: // 65 72 -> index=8 // 72 79 static size_t FREELIST_INDEX(size_t n){ return ((n + __ALIGN-1)/__ALIGN - 1); } // 65 72 -> 72 // 72 79 static size_t ROUND_UP(size_t bytes) { return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1)); } static void* ChunkAlloc(size_t size, size_t& nobjs);//獲取大塊內存 static void* Refill(size_t bytes); //填充自由鏈表 static void* Allocate(size_t n); //分配返回小內存塊 static void Deallocate(void* p, size_t n); //管理回收內存 private: enum {__ALIGN = 8 }; enum {__MAX_BYTES = 128 }; enum {__NFREELISTS = __MAX_BYTES/__ALIGN }; union Obj{ union Obj* _freelistlink; char client_data[1]; /* The client sees this. 用來調試用的*/ }; // 自由鏈表 static Obj* _freelist[__NFREELISTS]; // 內存池 static char* _startfree; static char* _endfree; static size_t _heapsize; }; //__DefaultAllocTemplate成員初始化 template <bool threads, int inst> typename __DefaultAllocTemplate<threads, inst>::Obj* __DefaultAllocTemplate<threads, inst>::_freelist[__NFREELISTS] = {0}; // 內存池 template <bool threads, int inst> char* __DefaultAllocTemplate<threads, inst>::_startfree = NULL; template <bool threads, int inst> char* __DefaultAllocTemplate<threads, inst>::_endfree = NULL; template <bool threads, int inst> size_t __DefaultAllocTemplate<threads, inst>::_heapsize = 0;
前面說了,當咱們需求的內存塊在所對自由鏈表的下標處沒掛有內存塊時,咱們就必須調用refill去填充自由鏈表了。申請時通常一次性申請20個內存塊大小的內存(可參加STL實現源碼)。
那又從那裏找呢?——固然內存池啦!分配這麼大塊內存到二級配置器就是如今來用的。能夠經過移動startFree指針快速地從內存池內給「切割」出來這一段內存,而後按照大小切成小塊掛在自由鏈表下面。在這個過程當中能夠直接將第一小塊內存塊返回給用戶,其他的再掛在自由鏈表下,方便下次分配了。
基於這樣思路就能夠將refill實現以下:
void* __DefaultAllocTemplate<threads, inst>::Refill(size_t bytes) { size_t nobjs = 20; /*默認從內存池取20塊對象,填充*/ //從內存池中拿到一大塊內存 char* chunk = (char*)ChunkAlloc(bytes, nobjs); if (nobjs == 1) /*只取到了一塊*/ return chunk; size_t index = FREELIST_INDEX(bytes); printf("返回一個對象,將剩餘%u個對象掛到freelist[%u]下面\n", nobjs-1, index); Obj* cur = (Obj*)(chunk + bytes); _freelist[index] = cur; for (size_t i = 0; i < nobjs-2; ++i){ Obj* next = (Obj*)((char*)cur + bytes); cur->_freelistlink = next; cur = next; } cur->_freelistlink = NULL; return chunk; }
注:chunkAlloc向內存池索要內存
到此,咱們好像就會有一個疑問。既然簡單移動startfree就能夠歡快的從內存池取到得一塊內存返回,那爲何又要一次性取20塊,返回一塊,將剩下那19塊掛到freelist對應位置下面呢?挨個掛上去還這麼麻煩!每次都直接從內存池返回一塊內存不是更歡快嗎?在這裏固然不用擔憂出現外碎片問題。由於在每次內存釋放時,能夠添加到咱們維護的自由鏈表上,繼續下次分配。
接下來就是chuncAlloc函數
它表示從內存池那一大塊內存,同時也儘量保證內存池像水池同樣有時刻有「水」。具體它遵循下面幾條方針:
到了最後,一級配置器基於它的out-of-memory處理機制,或許有機會釋放去其它的內存,而後拿來此處使用。若是能夠那就成功「幫助」內存池,不然便發出bad_alloc異常通知使用者。
基於這樣的思路,即可以模擬實現出ChunkAlloc函數
//function:從內存池申請一大塊內存 template <bool threads, int inst> void* __DefaultAllocTemplate<threads, inst>::ChunkAlloc(size_t size, size_t& nobjs) { size_t totalbytes = nobjs*size; size_t leftbytes = _endfree - _startfree; //a) 內存池中有足夠內存 if (leftbytes >= totalbytes){ printf("內存池有足夠%u個對象的內存塊\n", nobjs); void* ret = _startfree; _startfree += totalbytes; return ret; //b) 內存池僅剩部分對象內存塊 }else if (leftbytes > size){ nobjs = leftbytes/size; /*保存可以使用對象塊數*/ totalbytes = size*nobjs; printf("內存池只有%u個對象的內存塊\n", nobjs); void* ret = _startfree; _startfree += totalbytes; return ret; //c) 內存池中剩餘內存不足一個對象塊大小 }else{ // 1.先處理掉內存池剩餘的小塊內存,將其頭插到對應自由鏈表上 if(leftbytes > 0){ size_t index = FREELIST_INDEX(leftbytes); ((Obj*)_startfree)->_freelistlink = _freelist[index]; _freelist[index] = (Obj*)_startfree; } // 2.調用malloc申請更大的一塊內存放入內存池 size_t bytesToGet = totalbytes*2 + ROUND_UP(_heapsize>>4); _startfree = (char*)malloc(bytesToGet); printf("內存池沒有內存,到系統申請%ubytes\n", bytesToGet); if (_startfree == NULL){ //3. malloc申請內存失敗,內存池沒有內存補給,到更大的自由鏈表中找 size_t index = FREELIST_INDEX(size); for (; index < __NFREELISTS; ++index){ //自由鏈表拿出一塊放到內存池 if (_freelist[index]){ _startfree = (char*)_freelist[index]; //BUG ?? Obj* obj = _freelist[index]; _freelist[index] = obj->_freelistlink; return ChunkAlloc(size, nobjs); } } _endfree = NULL; /*in case of exception. !!保證異常安全*/ //逼上梁山,最後一搏. 若內存實在吃緊,則一級配置器看看out-of-memory可否盡點力,不行就拋異常通知用戶 _startfree = (char*)__MallocAllocTemplate<0>::Allocate(bytesToGet); } _heapsize += bytesToGet; _endfree = _startfree + bytesToGet; //遞歸調用本身,爲了修正nobjs return ChunkAlloc(size, nobjs); } }
這裏也還要注意一個點:就是_endfree= NULL
這樣一個操做
這句話很容易被咱們忽略掉。這實際上是十分重要的一個操做,這關乎到異常安全問題,在內存池窮山盡水之時,它取調用了一級配置器,但願一級配置器可否釋放一些內存,在chunkAlloc內能夠malloc成功,但一般這都是失敗的,因此一級配置器便拋出了異常,然而異常拋出並不意味着程序結束,此時的endfree並不爲NULL而且多是較大的數,(endfree保持之前的值)此時的startfree指針是爲NULL的。這二者的差值表示着內存池有着大塊的內存,然而這已不屬於內存池了。
不管alloc被定義爲第一級或第二級配置器,SGI還爲它包裝了一個接口Simple_alloc,使配置器接口符合STL規格:
#ifdef __USE_MALLOC typedef __MallocAllocTemplate<0> alloc; #else typedef __DefaultAllocTemplate<false, 0> alloc; #endif template<class T, class Alloc> class SimpleAlloc { public: static T* Allocate(size_t n){ return 0 == n? 0 : (T*) Alloc::Allocate(n * sizeof (T)); } static T* Allocate(void){ return (T*) Alloc::Allocate(sizeof (T)); } static void Deallocate(T *p, size_t n){ if (0 != n) Alloc::Deallocate(p, n * sizeof (T)); } static void Deallocate(T *p){ Alloc::Deallocate(p, sizeof (T)); } };
這裏面內部四個成員函數其實都是單純的轉調用,調用傳遞給配置器的成員函數,這個接口時配置器的配置單位從bytes轉爲了個別元素的大小。SGI STL中容器所有使用simple_alloc接口,例如
template< class T, class Alloc= alloc> class vector{ protected: //專屬空間配置器,每次配置一個元素大小 typedef simple_alloc<value_type, Alloc> data_allocator; void deallocate(){ if(...) data_allocator::deallocate(start, end_of_storage- start); } ... };
爲了將問題控制在必定複雜度內,到此以上的這些,僅僅處理了單線程的狀況。對於併發的狀況,它的處理過程會相對更復雜。咱們能夠查看STL中空間配置器的源碼實現來進一步的學習,這當中又會體現出不少優秀的思想,
STL配置器還有許多其它優秀設計,這裏只是本人對它的部分認識。爲了加深理解,咱們能夠查看STL中源碼進行更深刻學習。
模擬總體實現:https://github.com/tp16b/project/tree/master/alloc/src
參考:《STL源碼剖析》