本節將專注于TCMalloc 的架構設計細節,來整體看一下TCMalloc 的設計特性。
主要的幾個特性如下:
- 高性能。大多數對象的分配和釋放都不需要產生太多的競爭,因為tcmalloc 維護了thread-cache 來提供當前線程的內存分配需求。所以,應用大多數的內存申請需求不會有鎖的競爭,而且在多核場景有較好的擴展性。
- 靈活的使用內存資源。用戶不使用的內存,tcmalloc會選擇服復用或者歸還操作系統。
- 降低了每個請求的內存開銷。通過分配相同大小的page 降低內存使用的開銷,這在小對象場景較為有效。
- 內部信息統計開銷較低。能夠開啟細粒度的應用內存占用信息,來幫助用戶展示tcmalloc內部內存使用的細節。
如何使用
安裝tcmalloc,應用編譯加入 -ltcmalloc即可默認將glibc的malloc 替換為tcmalloc的malloc 。
架構概覽
前面的文章中在介紹整體的tcmalloc的時候已經介紹過了,大體分為三個部分:
- Front-end,主要維護線程cache,用來為應用提供 快速分配以及釋放內存的需求。
- Middle-end,主要負責為font-end 中的thread-cache 填充內存。
- back-end,負責從os直接獲取內存。
需要注意的是front-end 的 thread-cache 可以在每一個cpu下維護 或者 每一個線程下維護,back-end` 則能夠支持大內存的pageheap管理,也能支持小內存的pageheap 管理。
接下來,我們進入每一個組件詳細看一下其設計細節。
1. TCMalloc Front-end
Front-end 提供了Cache ,能夠緩存一部分內存 用來分配給應用 ,也能夠持有應用釋放的內存。這個Cache 作為per-cpu/per-thread 存在,其同一時刻只能由一個線程訪問,所以本身不需要任何的鎖,這也是多線程下內存分配釋放高效的原因。
如果Front-end 持有的內存大小足夠,其能夠滿足應用線程任何內存需求。如果持有的內存為空了,那它會從 middle-end 組件請求一批內存頁進行填充。
Middle-end 是由 CentralFreeList 和 TransferCache 兩部分組成,后續會詳細介紹這兩個組件。
如果用戶請求的內存大小超過了front-end 本身能緩存的大小(大內存需求),或者middle-end 緩存的內存頁也被用盡了,那這個時候會直接讓從back-end分配內存給用戶。
Front-end 在 TCMalloc 的演進過程中有兩種類型的實現:
- 最開始的時候只支持 per-thread 的cache 模式,這也是TCMalloc 名字 Thread-Cacheing malloc的由來。然而這種場景會隨著用戶線程的大量增加,出現了一些內存問題:每個線程只能有極小的thread-cache,需要消耗較多的CPU資源來聚合每個線程的內存資源。
- 較新版本的TCMalloc 支持了 per-CPU 模式。這種模式下每一個邏輯CPU 會有自己的的thread-cache 用來為運行在這個cpu的現場分配內存。在x86系統上,每一個邏輯cpu 和 一個超線程等價,我們lscpu 命令看到的cpu core是包括超線程的個數在內的。
1.1 小對象和大對象的內存分配過程
針對小內存對象的分配,Front-end的cache 會按照其大小將其映射到 60-80個size-classes 中的一個,實際的內存分配會按照該大小對應的size-class 的大小進行分配,size-class也是front-end 分配內存的粒度。
比如12B 的對象會best-fit到16B 的size-class中。設置這么多的size-class 還是為了盡可能得降低內存的浪費,比如原本的內存分配粒度都是2的n次冪,那對于23字節的內存需求就需要分配一個32字節的內存區域,而在tcmalloc的size-class的配置中只需要分配24字節即可。
Tcmalloc 在編譯的時候可以配置 size-class 的 內存分配是按照多少 Bytes 對齊,比如指定了__STDCPP_DEFAULT_NEW_ALIGNMENT__ <= 8,那size-class 內部可選的內存大小配置就是 8bytes 對齊,即8 bytes的倍數。如果編譯時指定了大于 8bytes,那后續所有的 ::operator new 的內存申請都會按照 16 bytes 進行對齊。
對于大內存需求的對象來說,內存大小的需求超過kMaxSize 256K,則會直接從back-end 分配。因此,這一部分的內存需求不會緩存再 front-end 以及 middle-end 中,由back-end的page-heap 進行管理。對于大內存對象的實際內存分配會按照tcmalloc page size 進行對齊。
1.2 內存釋放過程
如果要釋放一個對象,編譯期間如果能夠知道這個對象的大小,編譯器會直接告訴分配器這個對象的大小。大多數的時候,編譯期間并不清楚對象的大小,會從pagemap中查找這個對象。如果這個對象是一個小內存對象,釋放的時候會告訴front-end cache進行管理,如果是一個超過kMaxSize 的對象,則會直接釋放給back-end 的 pageheap。
1.3 Per-CPU mode
Per-cpu mode 和 per-thread mode 是 tcmalloc 的font-end 主體部分的兩種模式。因為per-thread mode 受到系統進程的線程數的影響,在大量線程的情況下會讓每個thread-cache 只能夠處理一小部分的內存申請釋放需求,還會消耗大量的cpu 來 由middle-end 進行不同thread-cache 之間的內存遷移。
所以 tcmalloc 提供了優化版本的 per-cpu mode,即每一個邏輯核 維護一個 ‘cpu-cache’ ,用來處理運行在當前核的線程的內存申請/釋放需求。
大體形態如下:
Per-cpu mode 下會申請一個大內存塊(也可以稱為slab),這個slab 會被多個cpu共享,其中每一個cpu會 持有slab 的一部分內存,并在其上存儲一系列元數據管理對應線程的內存對象。
上圖中的cpu1 會管理 on slab 的綠色部分內存區域,在這一部分區域中會先存儲一些元數據和指針來管理不同大小的 size-classes 的內存對象。其中元數據中包含一個header指針 和 每一個size-class 的索引block。
每一個size-class 的header 指針數據結構會有指向某一種實際存儲內存對象的指針數組,即是一個數組,每一個元素是一個指針,總共是三個指針,分別指向這一種size-class 內存對象區域的起始地址塊,當前地址塊(后續分配這個size-class 大小對象的時候會從current 開始分配),最大地址。
每一個cpu 能夠緩存的內存大小是通過SetMaxPerCpuCacheSize 配置的,也就是當前font-end 能夠緩存的內存總大小取決去當前系統的cpu核心數,擁有更好核心數的機器使用tcmalloc 能夠緩存更多的內存。為了避免 perf-cpu 長時間持有內存,tcmalloc 允許通過MallocExtension::ReleaseCpuMemory 接口來 指定釋放某一個cpu的內存。
void MallocExtension::SetMaxPerCpuCacheSize(int32_t value) {
#if ABSL_INTERNAL_HAVE_WEAK_MALLOCEXTENSION_STUBS
if (MallocExtension_Internal_SetMaxPerCpuCacheSize == nullptr) {
return;
}
MallocExtension_Internal_SetMaxPerCpuCacheSize(value); // 實際的執行函數
#else
(void)value;
#endif
}
// 釋放某一個cpu cache的內存
size_t MallocExtension::ReleaseCpuMemory(int cpu) {
#if ABSL_INTERNAL_HAVE_WEAK_MALLOCEXTENSION_STUBS
if (MallocExtension_Internal_ReleaseCpuMemory != nullptr) {
return MallocExtension_Internal_ReleaseCpuMemory(cpu);
}
#endif
return 0;
}
當每一個cpu cache 的內存被分配耗盡,想要從 middle-end 獲取內存來緩存更多的對象時,也需要考慮對size-class進行擴容。如果這個size-class 的內存分配需求還在持續增加,則這個size-class的容量會持續增加,直到達到這個size-class 容量的hard-code。
1.4 Per-thread mode
用戶可以指定使用某一個thread 的cache,也就是指定thread-local cache。小內存的申請和釋放會根據thread-cache的需求在middle-end 之間遷移。
每一個 thread-cache 內部 不同size-class 對象會各自構造一個單鏈表(如果有 n 個size-classes,也就會有對應 n 個單鏈表),類似如下圖:
分配某一個對應size-class 對象的時候,對應 size-class 鏈表對象會被從單鏈表移除(free-list),表示這個指針對應地址的內存可以被用戶使用。釋放對象的時候,則會將這個對象地址追加到thread-cache 管理的 size-class 的鏈表。
在這個過程中,如果thread-cache 管理的內存不夠,或者超限,則會從 middle-end 獲取更多的內存對象或者將多余的內存對象釋放給 middle-end。
對于per-thread caches來說,可以通過 MallocExtension::SetMaxTotalThreadCacheBytes 設置最大的可用內存大小。每一個線程有自己的最小的 thread-cache 大小 KMinThreadCacheSize 512K,如果當前線程內存申請需求較大,內存容量也會通過middle-end 將其他線程的可用內存遷移到當前線程。
通過 middle-end 來協調當前的thread-cache 內存,通過ThreadCache::Scavenge(); 進行。
如果當前線程退出,則會將自己的thread-cache 的內存返回給 middle-end。
1.5 per-cpu 和 per-thread 運行時內存管理算法對比
對于thread-cache 和 per-cpu cache來說,在應用程序運行的時候如果 front-end 的cache 內存太小,那就需要頻繁從 central-freelist 也就是 middle-end 中獲取內存;但如果太大,就會讓過多的內存被閑置。如何配置一個合理的 front-end cache 的大小,這里兩種模式都提供了動態配置 cache大小的算法。
- Per-thread 模式下,cache 內部的最大存儲對象容量 達到當前最大閾值時就會從middle-end 獲取更多的對象,從而增大這個限制。降低最大限制的前提是發現了較多的未被使用的對象,則會將這一些對象根據需求還給middle-end。
- Per-cpu 模式下,增加cache 容量的前提是當前cache 是否在頻繁的從 middle-end 獲取內存 以及 釋放內存交替,則需增加容量限制,有更多的空間來緩存這一些內存對象。降低容量限制的前提是發現有一些空閑容量長時間沒有被使用。
這里在代碼細節上有很多的設計,比如如何設置合理的空閑時間的長度?如何界定 內存申請是頻繁的?都是一些動態規劃的最優思想的設計,值得去探索代碼細節。
2. TCMalloc Middle-end
到此,font-end 部分大體設計描述完。從前面的設計中可以看到,middle-end 的作用在 per-cpu和per-thread 模式都有非常重要的作用。
Middle-end 的主要作用為 font-end 提供內存申請需求,并將空閑內存返回給 back-end。
Middle-end 的組成主要有 Transfer cache 和 Central free list。對于每一個size-class,都會有有一個各自的 transfer cache 和 central free list。這一些caches 會有自己的 mutex lock,所以對于這一些cache的訪問, 因為鎖粒度較低,則不會有過多的沖突,保證了訪問的性能。
2.1 Transfer Cache
當 front-end 請求內存 或者 釋放內存的時候,會先到達 transfer cache。
Transfer cache 會持有 一個數組指針進行內存的釋放 或者 將新的內存對象填充進來并返回給font-end。
Transfer cache 會將一個cpu/tthread 釋放的內存分配給另一個cpu/thread 對內存的需求,這個類似于內存池的 內存對象流動在兩個不同的cpu/threads 之間可以非常迅速。
2.2 Central Free List
Central free list 通過 span 數據結構來管理內存,一個span 可以管理一個或者多個tcmalloc page,span 數據結構會在下文詳細描述。
Font-end 如果從 central free list 請求一個或者多個內存對象的時候,central free list 會從span中提取對應大小的對象,如果此時span 沒有足夠的pages 返回,則會從back-end 請求更多的span。
當內存對象返回給central free list,則這一些對象會通過 pagemap 被映射到對應的span中進行管理,如果所有的對象都返回給span,這個span就可以被釋放給back-end.
2.3 Pagemap 和 Spans
tcmalloc 管理的堆內存會在編譯期間確定一個page-size,并將這么多內存映射為對應size的一個個page。一系列正在被使用的pages 可以被一個span 對象描述,一個span 對象可以管理一個大的內存對象,也可以按照size-class 管理多個小對象。
pagemap 則是用來查找一個內存對象屬于哪一個span的,或者申請一個指定size-class 的內存對象。pagemap 是一個 2層或者3層的 radix-tree。下圖展示了一個兩層的 page-map 如何管理 span的,其中 spanA 管理了2個page,spanB 管理了三個page。
Span 這個數據結構 在 middle-end中用來 管理回收的內存對象;在back-end 用來管理 對應大小的pages,負責給central-list 提供對應大小的span。
3. TCMalloc Backend
tcmalloc 的back-end 主要有三個工作線程:
- 管理未使用的大塊內存區域
- 負責從 os 中獲取內存,來滿足其他組件的內存需求
- 負責將其他組件不需要返回的內存,還給os
還有兩個后端組件:
- legacy pageheap. 管理tcmalloc 的pages
- 管理大頁內存的 pageheap。用來提升應用程序大頁內存的申請需求,降低TLB的miss。
3.1 Legacy Pageheap
legacy pageheap 是一個數組的freelist,統一管理可用內存頁。數組的每一個節點是一個free list,也就是單鏈表。一般這個數組的大小 k < 256,對于第k 個數組元素來說,它的鏈表中的每一個節點都管理了 k 個pages。
如果想要申請 k 個pages,則直接查找這個數組的第k 個元素,從free list中取一個節點即可。如果這個free list是空的,那就查找下一個數組元素的free list,直到找到一個非空的free list。如果還是失敗,則直接mmap 獲取內存。
當一段連續的pages 被返回給了pageheap,則會根據當前pages 是否能夠形成一個連續的pages區域,然后串聯這一些pages 并根據page數量 添加到對應的free list中。
3.2 大頁場景分配器設計
針對hugepage 的分配器設計 是希望其能夠有效持有 hugepage 大小的內存塊,需要的時候能夠快速分配,不需要的時候也能在合理的內存占用情況下釋放給操作系統。
hugepage 在x86 系統上一般是2M 及 以上的大小,tcmalloc 的back-end 擁有三個不同的cache 來管理大頁內存的分配:
- filler cache。能夠持有hugepage ,并提供一些大頁內存的申請需求。類似于legacy pageheap,通過一些free lists 管理pages 那樣管理huge page,主要用于處理小于hugepage 大小的內存申請。
- region cache。用于大于hugepage 大小的內存申請,這個cache 允許分配多個連續的hugepage。
- hugepage cache。和region cache的功能有點重復,也是用于分配大于hugepage 的內存申請。
-
內存
+關注
關注
8文章
3037瀏覽量
74151 -
操作系統
+關注
關注
37文章
6856瀏覽量
123447 -
編譯
+關注
關注
0文章
659瀏覽量
32913 -
malloc
+關注
關注
0文章
52瀏覽量
73
發布評論請先 登錄
相關推薦
評論