tcmalloc的工作原理(一)

btbb1989發表於2014-07-22

原文:http://blog.chinaunix.net/uid-24990614-id-3911071.html

本文根據How tcmalloc Works翻譯而來,作者是James Golick,原文地址:http://jamesgolick.com/2013/5/19/how-tcmalloc-works.html


前言


tcmalloc是一款專為高併發而優化的記憶體分配器。tcmalloc的tc含義是thread cache,tcmalloc正是通過thread cache這種機制實現了大多數情
況下的無鎖記憶體分配。
這可能是我有幸拜讀的最為精密設計的軟體,雖然我不能詳述tcmalloc的細節,但是我會盡量談及到tcmalloc的所有重點。

tcmalloc與大多數現代分配器一樣,使用的是基於頁的記憶體分配,也就是說,這種記憶體分配的內部度量單位是頁,而不是位元組。這種記憶體分配
可以有效地減少記憶體
碎片,同時,也可以增加區域性性。此外,也可以使得後設資料的跟蹤更為簡單。tcmalloc定義一頁為8K位元組,在大多數的linux
系統中,一頁是4K位元組,也就是tcmalloc
的一頁是linux的兩頁。

tcmalloc中的記憶體分配塊整體來說分為兩類,“小塊”和“大塊”,“小塊”是小於kMaxPages的記憶體塊,“小塊”可以進一步分為size classes,
而且“小塊”的記憶體分配
是通過thread cache或者central per-size class cache而實現。“大塊”是大於等於kMaxPages的記憶體塊,“大塊”的
記憶體分配是通過central PageHeap實現。



Size Class


一般來說,tcmalloc為“小塊”建立了86個size classes,每一個class都會定義thread cache的特性,碎片化以及waste特徵。

對於一個特定的size class,一次性分配的頁數就是一個thread cache特性。tcmalloc細緻地定義了這個頁數,使得central cache和thread cache
之間的轉換能夠
保持以下兩者的平衡:thread cache周邊的wasting chunk,以及訪問central cache太過於頻繁而導致的鎖競爭。定義這個頁數的程
序程式碼也保證了每一種class
的waste比例最多為12.5%,當然,malloc API需要保證記憶體對齊。

size class data存於SizeMap中,並且是啟動階段第一個初始化的物件。


Thread Caches


thread cache是一種惰性初始化的thread-local資料結構,每個size class包含一個free list(單向),此外,他們包含了表示他們容量的總大
小的後設資料。


在最佳的情況下,從thread cache分配記憶體和釋放記憶體是無鎖的,並且時間複雜度是線性的。如果當前thread cache不包含需要分配的記憶體塊時,
thread cache從central cache獲取那種class的記憶體塊。如果thread cache的空間太多,thread cache的記憶體塊會返還給central cache,每一個
central
 cache都有一個鎖,這個鎖用來減少記憶體返還時的競爭。

雖然thread cache會有記憶體塊的遷移,但是thread cache的大小會根據兩個有趣的方式限定在一個範圍內。

其一,所有的thread cache的總大小會有一個上限。每一個thread cache都會在記憶體塊的遷移,分配和釋放時跟蹤它當前的記憶體塊總容量。起初,
每一個
thread cache都會被分配相同的記憶體空間。但是,當thread cache的容量動態變化時,會有一個演算法使得一個thread cache可以從其他的
thread cache偷取
沒有使用的空間。

其二,每一個free list都有一個上限,這個上限會隨著記憶體塊從central cache遷入時以一種有趣的方式增加,如果list超過了上限,記憶體塊會釋
放給central
 cache。

當記憶體的釋放或者有central cache的記憶體塊的遷入而導致thread cache超過了上限,thread cache首先會試圖查詢free list中特別的headroom,
以檢查thread
 cache是否有多餘的需要釋放給central cache的記憶體。當free list滿足了條件時,被加進free list的記憶體塊就是多餘的 。如果
還是沒有空出足夠的空間時,
thread cache會從其他的thread cache中偷取空間,當然需要擁有pageheap_lock。

central cache有其自己的系統,用來管理tcmalloc整個系統中所有的cache。每一個要麼是1M位元組的記憶體塊或者是1個入口(大於1M位元組)。當
central cache
需要更多的空間時,他們可以使用thread cache類似的方式從其他的central cache偷取記憶體空間。當thread cache需要返還記憶體塊給
central cache,而central
 cache又已經滿了無法獲取更多的空間時,central cache會釋放這些記憶體物件給PageHeap,也就是起初central cache
獲取他們的地方。


Page Heap


PageHeap算是整個系統的根本,當記憶體塊沒有作為cache,或者沒有被應用程式申請時,他們位於PageHeap的free list中,也就是他們起初被
TCMalloc_SystemAlloc
分配的位置,最終又會被TCMalloc_SystemRelease釋放給作業系統。大塊”記憶體被申請時,PageHeap也提供了介面跟
蹤heap後設資料


PageHeap管理Span物件,Span物件表示連續的頁面。每一個Span有幾個重要的特性。

一,PageID start是由Span描述的記憶體起始地址,PageID的型別是uintptr_t。

二,Length length是Span頁面的數量,Length的型別也是uintptr_t。

三,
Span *next和Span *prev是Span的指標,當Span位於PageHeap的free list雙向連結串列中。

四,一堆更多的東西,但限於篇幅就不談了。

PageHeap有kMaxPages + 1的free list,span length從0到kMaxPages一一對應一個free list,大於kMaxPages的有一個free list,這些list都是
雙向的,並且分為了normal
和returned兩個部分。

一,normal部分包含這樣的Span,他們的頁面明確地對映到程式地址空間。

二,returned部分包含這樣的Span,他們的頁面通過呼叫含有MADV_FREE引數的madvise返還給作業系統。作業系統在必要的時候可以回收這些頁面,
但是當應用程式在
作業系統回收前使用了這些頁面,madvise的呼叫實際上是無效的。甚至當記憶體已經被回收,核心會重新把這些地址對映到一塊全
零的記憶體。因此,重新利用returned
的頁面不僅是安全的,而且還是減少heap碎片化的一種重要的策略。

PageHeap包含了PageMap,PageMap是一個radix tree資料結構,會對映到他們對應的Span物件。PageHeap也包含PageMapCache,PageMapCache會
對映記憶體塊的PageID
到他們在cache中的記憶體塊對應的size class。這是tcmalloc儲存後設資料的機制,而不是使用headers和footers對應實際的指標。
儘管這樣有些浪費空間,但是這樣在實質上可
以更有效地快取,因為所有相關的資料結構都被“slab”式地分配了。

PageHeap通過呼叫PageHeap::New(Length n)分配記憶體,其中n是需要分配的頁面數。

一,大於等於n的free list(除非n大於等kMaxPages)會被遍歷一遍,查詢是否有足夠大的Span。如果找到了這樣的Span,這個Span會從list移除,
然後返回這個Span,這
種分配是最合適的,但是因為地址不是有序的,因此從記憶體碎片化的角度來說是次優的,大概算是一種效能上的折中。normal
list會在繼續檢查returned list前全被檢查一遍。
但是我也不知道為什麼。

二,如果步驟一沒有找到合適的Span,演算法將會遍歷“大塊”list,並且查詢最合適的地址有序的Span。這個演算法的時間複雜度是O(n),它不僅會
遍歷所有的“大塊”list,在併發
大幅波動的情況下,這可能會非常耗時,而且還會遍歷有碎片的heap。我針對這種情況寫過一個補丁,當“大塊”
list超過了一個可配置的總大小時,通過
重組“大塊”list為一個skip list來提高應用程式的大記憶體分配的效能。

三,如果找到的Span比需要分配的記憶體大至少一個頁面尺寸時,這個Span會被切分為適合記憶體分配的尺寸,在返回分配記憶體塊之前,剩下的記憶體會
新增到合適的free list中。


四,如果沒有找到合適的Span,PageHeap會在重複這些步驟前嘗試增長至少n個頁面。如果第二次查詢還是沒有找到合適的記憶體塊,這個記憶體分配最
終會返回ENOMEM。


記憶體釋放是通過PageHeap::Delete(Span *span)實現,該方法的作用是把Span合併到合適的free list中。

一,從PageMap查詢該Span的相鄰Span物件(左和右,如果在一邊或者兩邊找到了free的記憶體,他們會從free list中去除,並且合併到Span中。

二,預先要知道span現在屬於哪個free list。

三,PageHeap會在檢查是否需要釋放記憶體給作業系統,如果確實需要,則釋放。

每次Span返還給PageHeap,Span的成員scavenge_counter_會減少Span的length,如果scavenge_counter_降到0,則從free list或者“大塊”list
釋放的Span會從normal list中去除,並新增到合適的
returned list中等待回收。scavenge_counter_被重置為:

min(kMaxReleaseDelay, (1000.0 / FLAGS_tcmalloc_release_rate
) * number_of_pages_released)。

因此,調整
FLAGS_tcmalloc_release_rate記憶體釋放時非常有用。


總結


一,這個部落格相當長,恭喜你看到了這裡,同時我覺得我似乎什麼也沒說。

二,如果你覺得這個問題很有趣,我非常推薦你看原始碼。雖然tcmalloc很複雜,但是程式碼寫得很易讀,也有很多註釋。我對C++不怎麼熟悉,但是
仍然寫了大量補丁,尤其以這篇博文為指南,tcmalloc就沒什麼可怕的了。

三,我將在接下來談論jemalloc。

四,可以關注我以及Joe Damato的podcast,裡面都是這種東西。

相關文章