FreeBSD的一個可擴展的併發malloc(3)實現

Jason Evans [email protected]
April 16, 2006

摘要

FreeBSD項目從版本5開始就致力爲爲多處理器計算系統持續提供可擴展支持。已經取得了足夠足夠的進展,C類庫的malloc(3)內存分配器是運行在多處理器系統的多線程應用的潛在瓶頸。在本文中,我們提供一個新的內存分配器,建立在現有技術的基礎上,爲應用提供一個可擴展的併發分配。基準測試表明該分配器,對多線程應用內存分配可以隨着處理器數量增加而擴展。同時,單線程分配性能與之前的分配器實現類似。

介紹

FreeBSD之前的malloc(3)由Kamp(1998)實現,通常被稱爲phkmalloc,長久以來被認爲最有用的選擇之一,並且相比已發佈的其他實現表現良好(Feng and Berger, 2005; Berger et al., 2000; Bohra and Gabber, 2001)。然而,它是在多處理器系統很稀少時設計的,並且對多線程的支持也是參差不齊。FreeBSD項目致力於在SMP系統上持續努力提供可擴展的性能,並且已經取得了充分的進展,malloc(3)已經成爲一些多線程應用的一個可伸縮擴展的瓶頸。本文提供一個新的malloc(3)實現,在這裏被非正式的稱之爲jemalloc。
表面上看,內存分配與聲明顯得似乎是一個簡單的問題,只不過爲了對可用的內存保持跟蹤,記錄它正在使用中,需要“記賬”一個標識位。然而,幾十年的研究和許多分配器的實現都沒有產生一個明顯更好的分配器。事實上,度量分配器性能最佳的已知實踐非常簡單,度量性能的彙總統計也是如此。Wilson et al等人. (1995)對十年前的技術提供了一個很好地回顧。那時多處理器並不是一個重要的問題,另外,該回顧還提供了現代分配器面對的問題的一個全面的總結。下面簡要介紹了特別影響jemalloc的各種各樣問題,但是並不企圖討論當設計一個分配器時所需要考慮的所有問題。
分配器性能通常由應用的執行時間組合與應用內存使用的平均值或峯值來度量。它並不能充分的單獨度量由分配器代碼消費的時間。由於CPU緩存,RAM,和虛擬內存分頁的影響,內存佈局對如何快速的使運行中的應用停頓有重要的影響。現在普遍認爲,綜合的跟蹤監控甚至不足以度量分配策略對碎片化的影響(Wilson et al., 1995)。分配器性能的唯一確定度量是通過度量應用實際的執行時間和內存使用獲得的。這使得在限定分配器性能特徵時形成了挑戰。考慮到一個分配器對於某些特定的分配模式可能執行的非常差,但是如果所有基準測試的應用沒有表現出任何此類模式,那麼這個分配器可能會表現執行的很好,儘管某些工作負載表現不佳。這使得在廣泛的多元化的應用中進行測試變得很重要。它還促進了分配器設計的一種處理方法,使退化邊緣情況的數量與嚴重程度最小化。
碎片可以分爲內部碎片和外部碎片。內部碎片是個別分配相關浪費空間的度量,由於未使用頭部或尾部空間。外部碎片是基於虛擬內存系統的物理空間的度量,但不是由應用直接使用。這兩種碎片對性能有不同特徵的影響,依賴於應用的行爲。理論上,所有碎片類型將會被最小化,但是分配器不得不進行一些權衡,從而影響每種類型碎片的發生次數。
過去的十年裏RAM成本明顯降低並且在更加豐富,所以phkmalloc被特別優化到更小的工作頁集,jemalloc必須更多的關注緩存局部性,並且擴展一下,CPU緩存線的工作集。分頁依然可能導致性能急劇下降(並且jemalloc不能忽視這個問題),但現在更普遍的問題是從RAM中獲取數據涉及到與CPU性能相關的巨大延遲。
一個比其他分配器使用更少內存的分配器,不一定能展示更好的緩存局部性,如果應用的工作集不適合緩存,如果工作集是緊湊的壓緊在內存中性能將會改善。適時地對象被分配的緊靠在一起,同樣傾向於它們在一起使用,那麼如果分配器可以連續的分配對象,那麼就有可能改進提升局部性。事實上,總內存使用量是緩存局部性的一個合理代理;jemalloc第一次嘗試最小化內存使用量,並且嘗試僅當它不與第一個目標衝突時分配連續的資源。
現代的多處理器系統在每個緩存線的基礎上保留了內存視圖的一致性。如果兩個線程同時運行在不同的處理器上並且修改在同一個緩存線裏的不同的對象,那麼這些處理器必須仲裁決定緩存線的所有權(圖1)。這類錯誤的緩存線共享可能引起嚴重的性能下降。一種解決這個問題的方法時填補對齊分配,但是填充對齊直接違背了將對象儘可能緊湊的壓緊在一起的目標;它可能造成嚴重的內部碎片。jemalloc在多分配競技場(Arena)上代替信任來減小這個問題,並且將它留給應用程序作者來填充分配,爲了避免在關鍵的性能代碼裏錯誤的緩存線共享,或者在代碼裏一個線程分配對象,並將對象傳遞給多個其他線程。
1

運行在多處理器系統上的多線程應用的分配器的一個主要目標是減少鎖競爭。Larson and Krishnan (1998)在展示與測試策略方面做的很好。 他們嘗試在他們的分配器裏上鎖,那並不是使用一個單獨的分配器鎖,每一個空閒列表有它自己的鎖。這有一些幫助,但是沒有足夠的擴展性,儘管最小化了鎖競爭。他們將此歸因於“緩存晃動” – 在操作分配器數據結構期間,處理器之間緩存數據的快速遷移。他們的解決方法通過使用多個分配器競技場(Arena),並且通過線程的唯一標識散列分配線程到各個競技場Arena(圖2)。這工作的很好,並且已經被其他實現使用(Berger et al., 2000; Bonwick and Adams, 2001)。jemalloc使用多競技場(Arena)方式,但是分配線程到各個競技場(Arena)動作使用了一個比散列更可靠的機制。
2

文章的剩餘部分描述了主要的jemalloc算法與數據結構,展示了在多處理器系統上多線程應用的性能與可擴展性度量指標的基準測試,與單線程應用的性能與內存使用量一樣,並且討論了內存碎片的度量指標。

算法與數據結構

FreeBSD支持實時的分配器配置通過配置文件/etc/malloc.conf,MALLOC OPTIONS環境變量,或者malloc選項的全局變量。這些提供了一個低開銷,無侵入配置機制,對調試與性能調優都有用。jemalloc使用這個機制支持phkmalloc支持的變量調試參數,以及公開的各種與性能相關的變量參數。
每個應用被配置在運行時有一個固定的競技場(Arena)數量。默認的,競技場(Arena)數量取決於處理器數量:
單處理器:所有分配器使用一個競技場(Arena)。使用多個競技場(Arena)是沒有意義的,因爲分配器內競爭僅可能發生在如果一個線程在分配期間被搶佔。
多處理器:使用競技場數量是處理器數量的4倍。通過分配線程至一個競技場集,單獨的競技場被併發使用的可能性就會降低。
第一次線程分配或釋放內存,它被分配到一個競技場。與線程的唯一標識散列法不同,競技場是以循環的方式被選擇的,這樣就可以保證所有競技場都有大致相同數量的線程分配給他們。線程唯一標識的可靠僞隨機散列(實際上,唯一標識就是指針)是衆所周知的困難,這也是最終促成了這種方法的原因。它依然存在線程與其他線程競爭一個特殊的競技場的可能,但是平均而言,初始化分配(可以理解爲一次性的靜態分配)不可能比循環分配更好。動態再均衡可能降低競爭,但是必要的記錄代價很高,而且很少會帶來足夠的好處來保證開支。
Thread-local存儲(TLS)對循環分配競技場的高效實現非常重要,因爲每個線程的競技場分配需要存儲在某個地方。Non-PIC code 與一些架構不支持TLS,所以在這些情況下,分配器使用線程的唯一標識散列。線程特定的數據(TSD)機制由pthreads類庫提供,是TLS的一個可行的替代選擇,除了FreeBSD的pthreads實現在內部分配內存外,如果分配器使用TSD將導致無限遞歸。
所有由sbrk(2) 或 mmap(2) 從內核請求內存都以“塊”大小的倍數進行管理,塊的base地址始終是塊大小的倍數(圖 3)。這樣塊的對齊允許對一個分配關聯的塊的計算是一個常量時間。塊通常由一些特殊的競技場(Arena)管理,並且觀察這些關聯對糾正分配器的功能很關鍵。塊大小默認爲2 MB。
3

分配大小的種類有三大類:small, large, and huge。所有分配請求都四捨五入到最接近的大小分類的邊界。Huge的分配比一個塊的一半還大,並且直接基於專用塊。關於huge的分配的元數據被存儲在一個單獨的紅黑樹。因爲大多數應用創建的huge分配很少,所以使用一個單獨的樹不會是可擴展性的問題。
對於small 與 large 的分配,使用二進制夥伴算法將塊切分爲頁運行。運行可以反覆的分成兩半,小到只有一頁,但是僅能以切分過程相反的方式合併。運行的狀態信息被存儲在一個頁map映射中在每個塊的開頭部分。通過將這些信息從運行中分離出來存儲,頁只有他們在使用它時纔會被觸及。這還允許將運行資源奉獻給大型分配,這些分配大於頁面的一半,但不大於塊的一半。
Small的分配分成3個子類目:tiny,quantum-spaced,與sub-page。現代架構根據數據類型對指針強加對齊約束。malloc(3)需要返回爲任何目的而適當對齊的內存。這種最壞情況下的對齊要求在這裏被稱爲量子大小(quantum size)(通常爲16字節)。在實踐中,2的冪次方對齊適用於tiny的分配,因爲他們不能夠包含足夠大的對象,需要量子對齊(quantum對齊)。圖4展示了所有分配大小的size分類。
4

通過除掉quantum-spaced這類尺寸,爲沒有子類目的small分配將會很簡單。無論怎樣,大多數應用主要分配的對象小於512字節,並且quantum spacing的尺寸類大大的降低了平均內部碎片。Large尺寸類可能會引起外部碎片的增加,但是實際上,降低的內部碎片通常大於外部碎片增長的偏移量
Small分配是分開的,這樣每次運行都管理一個單獨的尺寸類。區域位圖存儲在每次運行的開始,這比其他方法有幾個優勢:

• 位圖可以快速掃描到第一個空閒區域,這允許in-use(在用)區域的緊密打包。
• 分配器數據與應用數據是分開的。這降低了應用破壞分配器數據的可能性。這也可能增加了應用數據的局部性,因爲分配器數據沒有與應用數據混合在一起。
• Tiny區域可以更容易的支持。這將更加困難(如果使用其他方法),例如,如果一個免費列表是嵌在自由區域。
運行的報文頭部有一個潛在的問題:他們使用了應用程序原本可以直接使用的空間。這對於大於運行報文頭大小的尺寸類可能造成嚴重的外部碎片化。爲了限制外部碎片,所有都使用multi-page(多頁)運行,除了最小的尺寸類外。因此,對於最大的small尺寸類(通常爲2kb區域),外部碎片被限制在大概3%。
由於每次運行限制了它可以管理多少區域,所以必須爲每種尺寸類的多次運行做好準備。在任何給定的時間,每個尺寸類最多有一個"當前"運行。當前運行保留到直到它完全充滿或完成清空。考慮一下,如果沒有滯後機制,一個malloc/free可能導致運行的創建/銷燬。爲了消除這個問題,運行基於充滿度的四分位數分類,並且運行在QINIT的類別永遠不會被銷燬,它必須首先提升到一個更高的充滿度類別(纔可被銷燬)(圖5).
充滿度類別也爲從non-full(非滿)狀態的運行中選擇一個新的當前運行提供一個機制。優先級順序是: Q50, Q25, Q0, 然後 Q75. Q75 是最後的選擇,因爲像這類運行可能是幾乎完全滿的狀態;通常選擇像這種的運行可能導致當前運行快速週轉(可類比爲:上下文快速切換)。
5

原文

https://people.freebsd.org/~jasone/jemalloc/bsdcan2006/jemalloc.pdf

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