STL的內存管理allocator

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的倍數)從對應的空閒塊列表取表頭塊給用戶。 

allocator兩級分配器的優點: 

  • 小對象的快速分配。小對象是從內存池分配的,這個內存池是系統調用一次malloc分配一塊足夠大的區域給程序備用,當內存池耗盡時再向系統申請一塊新的區域,整個過程類似於批發和零售,起先是由allocator向總經商批發一定量的貨物,然後零售給用戶,與每次都總經商要一個貨物再零售給用戶的過程相比,顯然是快捷了。當然,這裏的一個問題時,內存池會帶來一些內存的浪費,比如當只需分配一個小對象時,爲了這個小對象可能要申請一大塊的內存池,但這個浪費還是值得的,況且這種情況在實際應用中也並不多見。 
  • 避免了內存碎片的生成。程序中的小對象的分配極易造成內存碎片,給操作系統的內存管理帶來了很大壓力,系統中碎片的增多不但會影響內存分配的速度,而且會極大地降低內存的利用率。以內存池組織小對象的內存,從系統的角度看,只是一大塊內存池,看不到小對象內存的分配和釋放。 

allocator實現的數據結構:

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; 
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鏈入空閒列表塊中; 
    } 
}

 

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)。

 

 

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