(一)簡介
tcmalloc是與glibc、malloc同一級別的內存管理庫,tcmalloc會hack所有glibc提供的接口,為調用者提供透明的內存分配。
(二)總體結構
- PageHeap
內存管理單位:span(連續的page的內存)
- CentralCache
內存管理單位:object(由span切成的小塊,同一個span切出來的object都是相同的規格)
- ThreadCache
線程私有的緩存,理想情況下,每個線程的內存需求都在自己的ThreadCache中完成,線程之間不需要競爭,非常高效。
內存管理單位:class(由span切成的小塊)
(三)分配與回收
基本思想:前面的層次分配內存失敗,則從下一層分配一批補充上來;前面的層次釋放了過多的內存,則回收一批到下一層次。
- 分配
1)小塊內存(<256KB)。
ThreadCache:先嘗試在list_[class]的FreeList分配。
CentralCache:找到對應class的tc_slots鏈表,從鏈表中分配 -> 從nonempty_鏈表分配(盡量分配batch_size個object)
HeadPage:伙伴系統對應的npages的span鏈表 (normal->returned)-> 更大的npages的span鏈表,拆小
kernel:申請若干個page的內存(可能大於npages)
2)大塊內存(>256KB)。
HeadPage:伙伴系統對應的npages的span鏈表 (normal->returned)-> 更大的npages的span鏈表,拆小
kernel:申請若干個page的內存(可能大於npages)
- 回收
與申請流程類似
1)ThreadCache => CentralCache
ThreadCache容量限額:
a、為每一個ThreadCache初始化一個比較小的限額,然后每當ThreadCache由於cache超限而觸發object到CentralCache的回收時,就增大限額。
b、預設所有ThreadCache的總容量,一個ThreadCache容量不夠時,從其他ThreadCache收刮(輪詢)。
c、每個ThreadCache也有最大最小值限制,不能無限增大限額。
d、每個ThreadCache超過限額時,對其每個FreeList回收。
單個FreeList的限額:
a、慢啟動。初始長度限制為1,限額1~batch_size之間為慢啟動,每次限額+1。
b、超過batch_size,限額按照batch_size整數倍擴展。
c、FreeList限額超限,直接回收batch_size個object。
2)CentralCache => PageHeap
只要一個span里面的object都空閑了,就將它回收到PageHeap。
3)PageHead中的normal => returned
a、每當PageHeap回收到N個page的span時(這個過程中可能伴隨着相當數目的span分配),觸發一次normal到returned的回收,只回收一個span。
b、這個N值初始化為1G內存的page數,每次回收span到returned鏈之后,可能還會增加N值,但是最大不超過4G。
c、觸發回收的過程,每次進來輪詢伙伴系統中的一個normal鏈表,將鏈尾的span回收即可。
(四)數據結構
- PageHeap
1)page到span的映射關系通過radix tree來實現,邏輯上理解為一個大數組,以page的值作為偏移,就能訪問到page對應的span節點。
2)為減少查詢radix tree的開銷,PageHeap還維護了一個最近最常使用的若干個page到object的對應關系cache。為了保持cache的效率,cache只提供64個固定坑位。
3)空閑span的伙伴系統為上層提供span的分配與回收。當需要的span沒有空閑時,可以把更大尺寸的span拆小;當span回收時,又需要判斷相鄰的span是否空閑,以便組合他們
4)normal和returned:多余的內存放到returned中,與normal隔離。normal的內存總是優先被使用,kernel傾向於一直保留他們;而returned的內存不常被使用,kernel內存不足時優先swap他們。
- CentralFreeList
1)維護span的鏈表,每個span下面再掛一個由這個span切分出來的object的鏈。便於在span內的object都已經free的情況下,將span整體回收給PageHeap;每個回收回來的object都需要尋找自己所屬的span后才掛進freelist,比較耗時。
2)empty的span鏈和nonempty的span鏈:CentralFreeList中的span鏈表有nonempty_和empty_兩個,根據span的object鏈是否有空閑,放入對應鏈表。如果span的內存已經用完則把這個span移到empty鏈表中。
3)通過頁找到對應span:被CentralFreeList使用的span,都會把這個span上的所有頁都注冊到radixtree中,這樣對於這個span上的任意頁都可以通過頁ID找到這個span。
4)如果span的內存已經完全被釋放(span->refcount==0),則把這個span歸還到PageHead中。
- ThreadCache
1)tcmalloc為每個線程創建一個ThreadCache對象,當線程結束的時候,ThreadCache對象會隨之銷毀。
2)ThreadCache為每種類型的內存都保存了一個單項鏈表。
(五)適用場景
tcmalloc適用線程內小內存分配需求,一般情況下只有大空間分配才使用中央堆,中央堆分配回收我記得是需要加鎖,成本高。
(六)使用遇到的坑
在一個項目中,使用thrift的threadpool模型+上游短連接+tcmalloc時,性能大幅下降,大概只有使用普通malloc的1/10。
效果對比圖如下:
但是同樣是使用tcmalloc,在短連接+多線程or長連接+線程池場景下,性能卻不受影響:
可以確定,是 tcmalloc+短連接+thrift線程池模型,才會出現這個坑。
使用oprofile工具對事件:1)分支預測錯誤;2)CPU時鍾;3)指令數;4)L2緩存未命中 采樣監控,觀察服務一段時間,如下:
可以看到,CPU時鍾占用的排序為:
tcmalloc::CentralFreeList::FetchFromSpans(spans->centralfreelist)、
tcmalloc::CentralFreeList::ReleaseToSpans(centralfreelist->spans)、
tcmalloc::ThreadCache::ReleaseToCentralCache(threadcache->centralcache)、
SpinLock::SpinLoop(分配pageheap時使用)、
tcmalloc::CentralFreeList::ReleaseListToSpans(centralfreelist->spans)
可以發現,服務過程中有頻繁的CentralFreeList與PageHeap之間的內存申請and回收,而tcmalloc的PageHead申請是使用的spinlock鎖,消耗大。