STL源碼剖析—空間配置器

來源:互聯網
上載者:User

看過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的預設記憶體 Clerk
      隱藏在這些容器後的記憶體管理工作是通過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_headif(free_list_head 不為空白){從該列表中取下第一個空閑塊並調整free_list,返回free_list_head}else{調用refill演算法建立空閑塊列表並返回所需的記憶體位址}}}// 演算法:refill// 輸入:記憶體塊的大小size// 輸出:建立空閑塊鏈表並返回第一個可用的記憶體位址{調用chunk_alloc演算法分配若干個大小為size的連續記憶體地區並返回起始地址chunk和成功分配的塊數nobjif(塊數為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_bytesif(記憶體池足以分配,即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中的記憶體 Clerk實際上是基於空閑列表(free list)的分配策略,最主要的特點是通過組織16個空閑列表,對小對象的分配做了最佳化。
1)小對象的快速分配和釋放。當一次性預先分配好一塊固定大小的記憶體池後,對小於128位元組的小塊記憶體配置和釋放的操作只是一些基本的指標操作,相比於直接調用malloc/free,開銷小。
2)避免記憶體片段的產生。零亂的記憶體片段不僅會浪費記憶體空間,而且會給OS的記憶體管理造成壓力。
3)儘可能最大化記憶體的利用率。當記憶體池尚有的空閑地區不足以分配所需的大小時,分配演算法會將其鏈入到對應的空閑列表中,然後會嘗試從空閑列表中尋找是否有合適大小的地區,
但是,這種記憶體 Clerk局限於STL容器中使用,並不適合一個通用的記憶體配置。因為它要求在釋放一個記憶體塊時,必須提供這個記憶體塊的大小,以便確定回收到哪個free list中,而STL容器是知道它所需分配的對象大小的,比如上述:
    stl::vector<int> array;
array是知道它需要分配的對象大小為sizeof(int)。一個通用的記憶體 Clerk是不需要知道待釋放記憶體的大小的,類似於free(p)。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.