內存分配器(Memory Allocator)

原文鏈接 : https://yq.aliyun.com/articles/254033

對於大多數開發者而言,系統的內存分配就是一個黑盒子,就是幾個API的調用。有你就給我,沒有我就想別的辦法。來UC前,我就是這樣認爲的。實際深入進去時,才發現這個領域裏也是百家爭鳴,非常熱鬧。有操作系統層面的內存分配器(Memory Allocator),有應用程序層面的,有爲實時系統設計的,有爲服務程序設計的。但他們的目的卻是一樣的,平衡內存分配的性能和提高內存使用的效率。


從瀏覽器開發的角度看,手機內存的增長速度相對於網頁內容的增長仍然只是溫飽水平,像Android本身就是用內存大戶,還有一個Low Memory Killer, 一定要優化內存佔用。總體上對策就是兩點:一是能不用就不用,代碼裏可能隱藏着很多不必要內存分配,特別留意那些中間量。二是能少用就少用,特別避免頻繁分配,因爲那樣只會增加內存碎片,到了極端時即使仍有內存可用,也分配不出來了。還有一個選項: 換個內存分配器。這樣一是如果內存分配器優良就可以緩解內存碎片,也可以在出現OOM時控制程序的行爲,崩與不崩、崩在哪裏就可以自己控制了。

 

最近因爲工作原因,涉及到了小內存分配器,所以做了一些粗淺的學習,沒有完整的閱讀代碼,也沒有進行透徹的測試,只是寫個總結以及相關的文檔放在這裏,備查。

 

內存分配的現實問題

 

首先通常使用的內存分配器,即malloc/free函數並不是系統提供的,而是C標準庫提供的。也被稱爲動態內存分配器。分配器從操作系統拿內存(虛擬內存)時是以頁爲單位(通常是4KB,調用sbrk或mmap), 然後再自行管理。

 

上面也提到了,內存分配器面對的是兩個核心問題: 效能和性能(或稱爲吞吐量Throughoutput)。 前者保證隨時有內存可用,後者保證服務時間短、不拖後腿。

 

對於一個系統進程而言,面對OOM(Out Of Memory)問題,排除程序使用內存的Bug外,會有兩個原因:
  1.系統真的沒有內存可用了。


  2.內存分配浪費了大量空間,雖然有大量零散的可用空間,卻無法合併提供出來使用。 前者纔是真正的OOM, 後者就是內存碎片(Fragmentation)問題了。

 

libc裏的malloc遇到分配失敗時,默認會abort掉進程,也就是崩掉(CRASH)了。如果系統支持mallopt就有機會改變這個行爲,可惜Android還沒有支持。
 

瀏覽器在加載、解析、渲染頁面的時候,會分配大量的小對象,看張圖就明白了:

    

 

上圖中模軸爲對象大小,縱軸則爲申請分配的次數。如果內存都以頁爲單位申請,就簡單,也就不需要分配器。就是那些小對象,佔用不多,使用頻繁,很容易造成頁內無法再繼續使用的碎片(Internal Fragmentation)。

 

對於性能,內存分配是次於I/O的一個瓶徑。雖然絕大多數情況下都相安無事,但內存分配器有一個重要的指標,即上限(bounded limits)。雖然平均值看起很好,但一旦遇到最壞的情況(wrost case)時,能不能保證性能?特別是多線程下,內存分配、釋放的性能常常受到加鎖的影響。有些分配器(如ptmalloc)過於考慮性能,而無法使線程間的內存共享,各自佔去一塊,反而降低了內存使用的效率。

 

這些問題一直存在,不同的人針對不同的場景設計出了不同的分配器算法(DSA, Dynamic Storage Algorithms, 是以應用的角度來看的),而且幾乎每一個都說自己比別人強。比如:
   1. dlmalloc/ptmalloc/ptmallocX C標準庫提供的分配器, 也是應用程序默認使用的malloc/free等函數。
   2. tcmalloc 出自Google, WebKit/Chrome中應用。
   3. bmalloc 畢竟Chrome和WebKit越走越遠,所以Apple在WebKit最新代碼(2014-04)裏提供了新的分配器,號稱遠遠超過 TCMalloc, 至少是在性能上。
   4. jemalloc 原本是爲FreeBSD開發的,後來Firefox瀏覽器和FaceBook的服務端都加以應用,它自身也在這些應用中得到了大幅提升。
   5. Hoard 一個專爲多線程優化的分配器, 作者是大學教授,有一些獨特的技術。Mac OS X中的malloc就有參考其實現進行優化。

*WebKit另外專爲Render Object提供了一個所謂的Plain Old Data Areana的類,也算是一個Memory Pool的實現(PODIntervalTree, PODArena)。

 

 

 

核心思想和算法

 

 

分配器這麼多,其核心思想相似,只是差在算法和metadata存儲上。附13提供的論文中有比較全面的總結,可以翻看一下。

 

內存分配器的核心思想概括起來三條:


1. 基本功能:首先將內存區(Memory Pool)以最小單位(chunk)定義出來,然後區分對象大小分別管理內存,小內存分成若干類(size class),專門用來分配固定大小的內存塊,並用一個表管理起來,降低內部碎片(internal fragmentation)。大內存則以頁爲單位管理, 配合小對象所在的頁,降低碎片。設計一個好的存儲方案,即metadata的存儲,減少對內存的佔用。同時優化內存信息的存儲,以使對每個size class或大內存區域的訪問的性能最優且有上限(bounded limits)。


比如dlmalloc定義的是一個個bins(同size class)來存儲不同大小的內存塊:

     
2. 回收及預測功能: 當釋放內存時,要能夠合併小內存爲大內存,根據一些條件,該保留的就保留起來,在下次使用時可以快速的響應。不需要保留時,則釋放回系統,避免長期佔用。


3. 優化多線程下性能問題:針對多線程環境下,每個線程可以獨立佔有一段內存區間,被稱爲TLS(Thread Local Storage),這樣線程內操作時可以不加鎖,提高性能。下圖是MSDN上貼出的關於TLS的原理圖,可以參考:

        

 

*另外測試工具也是必不可少,比如tcmalloc的heap profile, jemalloc則結合valgrind。FireFox在移植jemalloc到Android時,特別關掉了TLS,想必是考慮到它對於線程單一應用的副作用。

 

上面這些思路對於各個分配器而言基本是一致,但具體如何組織size classes, 如果以一個固定步長,必將形成一個巨大且效率低下的表,原因參考第一張圖就明白了。很多年前,就有專門的論文對此做了評定(鏈接)。另外還有如何定位內存塊? 如何解決多線程下的false cache line問題? 不同的分配器使用了不同的算法和數據結構來實現。它們所使用的算法統稱爲DSA, Dynamic Storage Algorithms。

 

具體的算法實現可以在下面的參考列表中找到對應的文檔, 也可以先看附16,文中分別對DSA Algorithms和DSA Operational Model做了描述,概括的很好,會有一個總體的印象。作者將DSA算法分爲五類:

  1. Sequential Fit

     是基於一個單向或雙向鏈表管理各個blocks的基礎算法,因爲和blocks的個數有關,性能比較差。這一類算法包括Fast-Fit, First-Fit, Next-Fit, and Worst-Fit。

  2. Segregated Free List (離散式空閒列表) 

     使用一個數組,每個元素是存儲特定大小內存塊的鏈表,它們所代表的大小並不是連續的,所以稱爲離散。經典的dlmalloc使用的就是這個算法。數據元素,參照上面的圖就可以理解了。TLSF算法則是基於此進行了改進。

  3. Buddy System

    這是由一代大師Donald Knuth提出,後續產生許多的改進版本。最大的作用是解決外部碎片(external fragmentation), 詳細的算法,參考這篇(淺析Linux內核內存管理之Buddy System)。

  4. Indexed Fit

   以某種數據結構爲每個block建立索引,以求可以快速存取。一般以一個二叉樹結構實現。比如使用Balanced Tree的Best Fit allocator, 以及基於Cartesian tree 的Stephenson Fast-Fit allocator。這類算法的性能比較高,也比較穩定。

  5. Bitmap Fit

   這類算法只是索引方法不同,使用以位圖式字節表示存儲單元的狀態。它的好處是使用一小塊連續的內存,響應性能更好。Half-Fit就屬於這類算法。

隨着技術演進,現在主流的allocators, 基本上都是綜合運用了兩類以上的算法。

 

另外一些基礎算法也是相似的,比如以二叉樹組織列表的算法,也就是in-place, 笛卡爾樹 和red-block的差異。在線程上,則因爲實現的不同,會導致內存佔用的差異。比如jemalloc在釋放時,並不需要在原來的分配線程執行釋放,只是被放回到分配線程的free list中去,ptmalloc則必須回到分配線程裏執行釋放,性能就相對弱一些。 tcmalloc則設計了算法,讓一個線程可以從它的鄰居那裏偷一些空間來(這個過程稱爲transfer cache),這樣可以有效地利用線程間的內存。

 

優劣勢對比

ptmalloc 劣勢:多線程下的性能及內存佔用(線程間內存無法共享),並且內存用於存儲metadata開銷較大,在小內存分配上浪費比較多。優勢:算是標準實現。

tcmalloc 劣勢:因爲算法的設計,佔用的內存較大。優勢:多線程下的性能。參考附6。

jemalloc 優勢: 內存碎片率低,多核下性能較tcmalloc更好。參考附17。

 

時間有限,沒有再深入研究,後面有空再補充一下。在實際應用中,還是有一些參數可以調整的,前提是要熟悉其實現,特別是性能評估的方法。

 

轉載請註明出處: http://blog.csdn.net/horkychen

 

參考

 

這是我列的最長的參考清單了,前人的確已經做了很多的研究,我對其中內容只是泛讀,並不是所有內容都相關,只是覺得有些內容可以相互應證就也列進來了。
1. jemalloc關於使用red-block tree的反思 [鏈接] 
  文章發佈於2008年,作者在2009年將其應用於FaceBook時,則是進行了算法上優化。
2. 2011年jemalloc作者在FaceBook應用jemalloc後撰文介紹了jemalloc的核心算法及在Facebook上應用效果。[鏈接] [早期的論文,有更多的細節]
3. Android碎片化的度量 通過改造ROM做的實驗。
4. Hoard Offical [鏈接]
5. Mac OS上malloc是怎麼工作的[鏈接]
6. 關於WebKit應用tcmalloc的對比[鏈接]
7. How tcmalloc works[鏈接] [中文翻譯]
8. TCMalloc源代碼分析,很不錯資料。作者的網站還有其它乾貨值得一讀。[鏈接] 
9. dlmalloc早期的技術文檔,講述了其核心算法。[鏈接]
10. ptmalloc源碼分析,講的很系統,非常值得一讀。[CSDN下載鏈接]
11. 介紹jemalloc的資料《更好的內存管理-jemalloc》[鏈接]
12. 替換系統malloc的四種方法 [鏈接]
13. 介紹針對實時系統進行優化的內存分配算法TLSF,其中對動態分配算法(DSA)做了總結。[鏈接]
14. 維基百科上關於Thread Local Storage的說明, 也許你能感受到技術的相通性。[鏈接]
15. 針對實時系統進行各種分配算法的對比,可以結合13一起看。[鏈接]
16. ptmalloc,tcmalloc和jemalloc內存分配策略研究。[鏈接]
17. Firefox3使用jemalloc後的總結,可以看到Firefox優化的思路。[鏈接] [Firefox使用的源代碼] 
18. Chromimum Project: Out of memory handling, 裏面有不錯的觀點。 [鏈接]

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