# Linux 核心專題: TLSF 實作
> 執行人: cin-cout
> [專題解說錄影](https://youtu.be/FirarrsHwrg)
> GitHub:
> * [cin-cout/tlsf-bsd](https://github.com/cin-cout/tlsf-bsd)
> * [cin-cout/allocator_benchmark](https://github.com/cin-cout/allocator_benchmark)
## 任務簡述
重做[第 6 週測驗題](https://hackmd.io/@sysprog/linux2023-quiz6)的測驗四,搭配第 5 週教材提及的 [tlsf-bsd](https://github.com/jserv/tlsf-bsd),探討何以 $O(1)$ 時間複雜度可落實於記憶體配置器。
## 背景知識
建議把老師上課影片看完,要自己理解對齊以及 overhead 等實作其實蠻沒有效率的,原本嘗試自己理解看得我一頭霧水,浪費了不少時間。
[你所不知道的 C 語言:記憶體管理、對齊及硬體特性篇](https://hackmd.io/@sysprog/c-memory)
測驗五的第一題是一個基本的 first fit 記憶體配置器的實作,先理解完畢有助於更加了解記憶體配置器的程式,但第一題的程式碼存在不少問題。
[2023q1 第 5 週測驗題 - 第一題](https://hackmd.io/@sysprog/linux2023-quiz5)
以下是解釋非常不錯的同學們的開發紀錄:
[yanjiew](https://hackmd.io/@yanjiew/linux2023q1-quiz5)
[ItisCaleb](https://hackmd.io/@ItisCaleb/linux2023q1-quiz5)
[WangHanChi](https://hackmd.io/@wanghanchi/linux2023-quiz5#%E6%B8%AC%E9%A9%97%E4%B8%80)
## TODO:
1. 研讀教材並紀錄認知及問題
至少要涵蓋:
* [TLSF: a new dynamic memory allocator for real-time systems](http://www.gii.upv.es/tlsf/files/ecrts04_tlsf.pdf)
* [Benchmarking Malloc with Doom 3](https://www.forrestthewoods.com/blog/benchmarking-malloc-with-doom3/)
* TLSF: [Part 1: Background](https://brnz.org/hbr/?p=1735), [Part 2: The floating point](https://brnz.org/hbr/?p=1744)
2. 解讀和改進程式碼
重做[第 6 週測驗題](https://hackmd.io/@sysprog/linux2023-quiz6)的測驗四,搭配上述來解釋何以 $O(1)$ 時間複雜度可落實於記憶體配置器,又如何改進實作。
3. Linux 核心一度具備同為 $O(1)$ 時間複雜度的 [xvmalloc](https://github.com/torvalds/linux/commit/644bf7b5983cf2540b57a5b25b775cb3c1e8e943),不過隨後就被 [zsmalloc](https://www.kernel.org/doc/html/v5.0/vm/zsmalloc.html) 取代,請依據 git log 和 LWN 等第一手材料,探討核心開發者的考量因素。
在討論時,避免「舉燭」,應至少將 [xvmalloc](https://github.com/torvalds/linux/commit/644bf7b5983cf2540b57a5b25b775cb3c1e8e943) 移植到 userspace,搭配 [tlsf-bsd](https://github.com/jserv/tlsf-bsd) 的測試和效能評比框架,分析其效益。
## 研讀 [Benchmarking Malloc with Doom 3](https://www.forrestthewoods.com/blog/benchmarking-malloc-with-doom3/)
該文講解怎麼實作偵測 alloc time 的方式,並在自行開發的遊戲上比較不同 alloc 方法的效能差距,最後再特別討論 alloc 對於聲音的 latency 之影響。
在了解 tlsf 實作前,先了解不同 alloc 方法的效能差距。
比較幾種常用的記憶體配置器:
* crt: 全名 C Runtime,是 Windows 內建的記憶體配置器。
* rpmalloc: 由 Epic Games 員工開發出的 lock-free GPA (Global Page Allocator)。
* tlsf: 著名的 pool allocator,需要 pre-allcating pool 且只支援 single-threaded。
### alloc
<div style="display: flex;">
<img src="https://hackmd.io/_uploads/r1UFugCrn.png" width="50%" height="50%">
<img src="https://hackmd.io/_uploads/rJ5tdeRrh.png" width="50%" height="50%">
<img src="https://hackmd.io/_uploads/rJgcugCS2.png" width="50%" height="50%">
</div>
### free
<div style="display: flex;">
<img src="https://hackmd.io/_uploads/HyP6cxCrh.pn" width="50%" height="50%">
<img src="https://hackmd.io/_uploads/r1jAqgAB2.png" width="50%" height="50%">
<img src="https://hackmd.io/_uploads/HJBesl0B2.png" width="50%" height="50%">
</div>
可以看到無論在 alloc 或是 free 的部分,crt 相比於 rpmalloc 和 tlsf 的平均時間都還要大,而在 size 較大時花費的時間也遠大於 rpmalloc 和 tlsf 。而 rpmalloc 的大部分資料的平均時間比 tlsf 來的小,但是在 size 較大時 (worst case) 表現卻遠不如 tlsf,在 worst case 中, tlsf 的表現相當亮眼。
rpmalloc 的 lock-free 可以減少執行緒之間的競爭,提高多執行緒環境下的性能表現,所以在現代強調多處理器、平行運算的計算機領域非常受歡迎。而 tlsf 雖然需要 pre-allcating pool 且只支援 single-threaded,但是也因為這些限制,在 worst case 中表現亮眼,對於一些精確度要求高、需要即時反應、且硬體規格較簡單(單核)的系統中, tlsf 非常合適。
## TLSF 架構
### mapping 技術
TLSF 跟最基本的記憶體配置器不同,使用一個 free list 串接所有的 free block ,詳見[測驗五第一題](https://hackmd.io/@sysprog/linux2023-quiz5),而是依照 free block 的大小,將每堆類似大小的 free block 建立一個 free list。所以每次在 alloc 一個新的空間時,都直接去最適合的大小的 free list 中尋找 free block,從而實現 good fit,而 **mapping 技術就是用來尋找最適合的 free list**。
TLSF (Two-Level Segregated Fit) 中使用二級的結構,加快記憶體存取速度並減少記憶體碎片化 (fragmentation)。
:::warning
a power of 2 的翻譯是「2 的冪」,不用加「次方」
:::
* 第一級:將記憶體劃分為 2 的冪大小的類別。例如,若第一級為 $2^6$,則將記憶體的範圍劃分為 $[2^6, 2^7)$。
* 第二級:將第一級所表示的範圍再劃分為 4 個等距離的區塊,從而善用記憶體空間
舉例來說,假設第一級為 $2^6$,表示記憶體的大小範圍為 $[64, 128)$。第二級會將這個範圍劃分為四個大小相等的區塊,每個區塊的大小為 16。這樣,第二級會將記憶體的大小範圍細分為 $[64, 80)$, $[80, 96)$, $[96, 112)$, $[112, 128)$。
> 注意: 這邊的意思不是用 fl 與 sl 將記憶體分 block ,它的意義是每一層 fl sl 都會儲存一個 free list,我們找到合適的 fl sl 是為了找大小最合適的 free list。
舉例:我們要 alloc 一個 size 為 $2^6 + 17$ 的空間,我們這邊先不加入提級層級的概念,後面會提到。mapping 會找到 fl sl (圖中 fl=2 sl=2)對應在 $2^6 + 17$ 的位置所記錄的 free list ,從這個 free list 中找 free block 分配空間。
<img src="https://hackmd.io/_uploads/HkgY6d1I3.png" width="70%" height="70%">
:::warning
注意用語,參見:
* [資訊科技詞彙翻譯](https://hackmd.io/@sysprog/it-vocabulary)
* [詞彙對照表](https://hackmd.io/@l10n-tw/glossaries)
:::
### 整體概念
這邊畫一張圖來講解運作中的 tlsf 可能會長怎樣,以便後續再講解程式碼時比較知道狀況。
```
+------------+
|其他管理TLSL | second level
|的參數 | / \
+------------+---------------------------> +-----------+ +-----------+ \
| block[][] | | fl 0 sl 3 | | fl 0 sl 0 |
+------------+----+(free list a)<---------- |對應大小 a~b| ... |對應大小 w~x| first
| free block |----|--+ +-----------+ +-----------+ level
| (size a~b) | | | .
+------------+ | | .
| blockA | | | .
+------------+----|--|----+(free list b)<-- +-----------+ +-----------+
| free block |<---|--|----|--+ |fl 32 sl 3 | |fl 32 sl 0 |
| (size c~d) | | | | | |對應大小 c~d| ... |對應大小 y~z|
+------------+ | | | | +-----------+ +-----------+ /
| blockB | | | | |
| | | | | |
+------------+<---|--|----+ |
| free block |----|--|-------+
| (size c~d) | | |
+------------+ | |
| blockC | | |
+------------+<---| |
| free block |-------+
| (size a~b) |
+------------+
```
## [TLSF](https://github.com/jserv/tlsf-bsd) 實作解析
以下將解析老師實作的 [jserv/tlsf](https://github.com/jserv/tlsf-bsd) 。
### 重要常數
64 位元系統對齊的 shift 為 3,亦即以 8 的倍數對齊,符合一個 word_size 大小。
32 位元系統對齊的 shift 為 2,亦即以 4 的倍數對齊,符合一個 word_size 大小。
```c
/* All allocation sizes and addresses are aligned. */
#define ALIGN_SIZE ((size_t) 1 << ALIGN_SHIFT)
#if __SIZE_WIDTH__ == 64
#define ALIGN_SHIFT 3
#else
#define ALIGN_SHIFT 2
#endif
```
```c
/* First level (FL) and second level (SL) counts */
/*表示使用 4 位元來表示 SL 索引的位移量。這意味著 SL 索引的範圍為 0 到 15。*/
/*通常這個值設定在 4 或 5 。*/
#define SL_SHIFT 4
/*每一層 FL 的 SL 數量*/
#define SL_COUNT (1U << SL_SHIFT)
/*FL 最大的 index ,與系統為 64 或 32 有關。*/
/*可以增加其大小,以來處理更大的記憶體配置。*/
#define FL_MAX _TLSF_FL_MAX
/*FL[1] 的位置,詳細解釋見下。*/
#define FL_SHIFT (SL_SHIFT + ALIGN_SHIFT)
/*FL 的數量,詳細解釋見下。*/
#define FL_COUNT (FL_MAX - FL_SHIFT + 1)
```
**以下以 32 位元的系統舉例:**
雖然最多可以支持 (1 << FL_INDEX_MAX) 位元的大小分配。 但是,因為要以 4 為倍數對齊,所以小於 `SL_INDEX_COUNT * 4` 的大小建立一級列表沒有意義 (根本無法劃分出足夠數量且又滿足對齊的 SL )。
`SL_INDEX_COUNT * 4` = `(1 << (SL_INDEX_COUNT_LOG2 + 2 ))`
= `(1 << (SL_INDEX_COUNT_LOG2 + 2 ))` = `(1 << (SL_INDEX_COUNT_LOG2 + ALIGN_SHIFT ))`
所以 FL_SHIFT = SL_SHIFT + ALIGN_SHIFT。
圖例:
```
以 ALIGN_SHIFT 為 2 ,SL_SHIFT 為 4 舉例。
┌─────────────────────┬─────────────┬──────────┬──────┐
│ ... | 256~128 | 64~128 | 0~64 |
└─────────────────────┴─────────────┴──────────┴──────┘
f[2] f[1] f[0]
若無此機制,f[0] 只會為 0~1,f[1] 為 1~2。
```
所以 FL_COUNT 的演算法為 FL_MAX - FL_SHIFT + 1,因為要扣掉前面 SHIFT (0~64) 的位元已經整合為 FL[0], 加 1 則是把加上 FL[0]。
而由於 SHIFT (0~64) 的位元已經整合為 FL[0],FL[0] 區段不能適用於通用的演算法,所以需要 alloc 在 FL[0] 要額外定義,在這個例子中,<64 bytes ,我們定義為 BLOCK_SIZE_SMALL ,小於 BLOCK_SIZE_SMALL 的大小,我們會直接 alloc 在 FL[0]。
```c
#define BLOCK_SIZE_SMALL ((size_t) 1 << FL_SHIFT)
```
### block 資料結構
先看 block 資料結構會比較理解前面一些關於 block 的常數。
#### block 的資料結構定義
這邊是對於記憶體配置器的概念的基本解釋,詳見[測驗五第一題](https://hackmd.io/@sysprog/linux2023-quiz5),先不考慮 prev。
此段解釋參考 [yanjiew](https://hackmd.io/@yanjiew/linux2023q1-quiz5)。
alloc 的空間為系統由 size 和 payload 組成。malloc 會回傳 payload 部份的指標。payload 為可以利用的空間,size 的值即為 payload 的大小。
```
--------------------------------------------------
| size | payload |
--------------------------------------------------
```
未配置的空間除了 size 外,後面還會接續 prev 和 next 二個指標,分別指向上一個和下一個未配置空間,size 的值即為 prev + next + 空閒位置的大小。
```
--------------------------------------------------
| size | prev | next | .......... |
--------------------------------------------------
```
上述二種狀態都可用 tlsf_block 結構來表示。size 為已配置和未配置空間都會使用的欄位,而 prev 和 next 只會在未配置空間使用。
```c
typedef struct tlsf_block {
/* Points to the previous block.
* This field is only valid if the previous block is free and is actually
* stored at the end of the previous block.
*/
struct tlsf_block *prev;
/* Size and block bits */
size_t header;
/* Next and previous free blocks.
* These fields are only valid if the corresponding block is free.
*/
struct tlsf_block *next_free, *prev_free;
} tlsf_block_t;
```
跟一般記憶體配置器最大的差別在多了 prev 這個指標,用來指向實體上的前一個 block,但他的儲存位置很特別,見下圖,取自 [esp-idf 的記憶體管理: TLSF 演算法](https://blog.csdn.net/gzxb1995/article/details/124504705)。
<img src="https://hackmd.io/_uploads/ByoT05SI3.png" width="50%" height="50%">
這裡的 `prev_phys_block` 就是 prev,block 是可以找到 prev 的,但其實 prev 是儲存在實體上一個 block 內,當上一個 block 被 alloc 後寫入資料,是可能蓋掉 prev 的,因為 prev 只是作為未被配置的記憶體合併時使用。而空閒 tlsf_block 的 size 指的是 next_free + prev_free + 空的位置 + prev(實體上下一個 block 的 prev)。
再回來看上面的常數設定。
```c
/*一個 ptr 的大小。*/
#define BLOCK_OVERHEAD (sizeof(size_t))
/*一個 block 必須足夠大以存儲其標頭減去 prev 的大小(儲存在上一個 block)。*/
#define BLOCK_SIZE_MIN (sizeof(tlsf_block_t) - sizeof(tlsf_block_t *))
/*不能大於 FL_INDEX 的可尋找的最大位數*/
#define BLOCK_SIZE_MAX ((size_t) 1 << (FL_MAX - 1))
/*見上段*/
#define BLOCK_SIZE_SMALL ((size_t) 1 << FL_SHIFT)
```
最後如同[測驗三](https://hackmd.io/@IFhfI6MRSZSja7n1T2-DNg/quiz3),因為現在電腦多數都是 32 bits (4 bytes) 以上,所以其位置二進位最後二位必為 0,不會使用到,所以 `block->header` 的第一個 bit 用來儲存目前的 block 是否為 free,第二個 bit 用來儲存前一個的 block 是否為 free。
```c
#define BLOCK_BIT_FREE ((size_t) 1)
#define BLOCK_BIT_PREV_FREE ((size_t) 2)
#define BLOCK_BITS (BLOCK_BIT_FREE | BLOCK_BIT_PREV_FREE)
```
`block_size`, `block_set_size`, `block_set_prev_free`, `block_set_free` 等函式都是對 header 作對應的 bitmask 操作,得到或更改想要的 bit ,實作很簡單,不一一解釋。
對幾個比較難懂的 block 操作做解釋:
#### payload 以及 block 位址互找
很直觀,見下圖,取自 [esp-idf 的記憶體管理: TLSF 演算法](https://blog.csdn.net/gzxb1995/article/details/124504705)。
<img src="https://hackmd.io/_uploads/SJCkqoSL2.png" width="50%" height="50%">
block + prev 的大小 + size 的大小 = payload 的位址,反之亦然。
offsetof 會獲取 `tlsf_block_t` 圖中 size (header) 成員相對於結構體起始地址的偏移量,即為 prev 的大小。
BLOCK_OVERHEAD 即為圖中 size (header) 的大小。
```c
INLINE char *block_payload(tlsf_block_t *block)
{
return (char *) block + offsetof(tlsf_block_t, header) + BLOCK_OVERHEAD;
}
...
INLINE tlsf_block_t *block_from_payload(void *ptr)
{
return to_block((char *) ptr - offsetof(tlsf_block_t, header) -
BLOCK_OVERHEAD);
}
```
#### prev / next block
找 block **實體上**前一個或後一個的 block。
> Note: 一直強調實體上,是跟 free list 內的前後 block 做對比。
block_prev 因為本來就有存 prev ,直接呼叫即可。
```c
/* Return location of previous block. */
INLINE tlsf_block_t *block_prev(const tlsf_block_t *block)
{
ASSERT(block_is_prev_free(block), "previous block must be free");
return block->prev;
}
```
示意圖如下:
```
+-----------+ <- block
| prev |
+-----------+
| header |
+-----------+ <- block_payload(block)
| next_free |
+-----------+
| prev_free |
+-----------+
| ... |
+-----------+ <- next (want we want)
| prev | } BLOCK_OVERHEAD
+-----------+ <- block_payload(block) + block_size(block)
| header |
+-----------+
| next_free |
+-----------+
| prev_free |
+-----------+
| ... |
+-----------+
```
要注意 `BLOCK_OVERHEAD` 表達的是一個 ptr 的大小,所以在上面對齊時當作 `header` 的大小,這邊則當作為 `prev` 的大小,可以將其替換成 `siezof(tlsf_block *)`。
```c
/* Return location of next existing block. */
INLINE tlsf_block_t *block_next(tlsf_block_t *block)
{
tlsf_block_t *next =
to_block(block_payload(block) + block_size(block) - BLOCK_OVERHEAD);
ASSERT(block_size(block), "block is last");
return next;
}
```
### tlsf_t 資料結構
接下來,介紹另一個重要的資料結構,名為 tlsf_t ,其意義為整個 tlsf memory pool 的 controller,以下用程式碼以及圖例會更好的理解。
```c
typedef struct {
uint32_t fl, sl[_TLSF_FL_COUNT];
struct tlsf_block *block[_TLSF_FL_COUNT][_TLSF_SL_COUNT];
size_t size;
} tlsf_t;
```
#### fl, sl[_TLSF_FL_COUNT]
fl 以及 sl 的 bitmap 用來記錄其對應的 sl 或 fl 是否存在 free block。 sl 中 1 表有 free block 0 表沒 free block,而在 fl 中 1 表所有此 fl 層中的 sl free block 0 表所有此 fl 層中的 sl 沒 free block 。
```
舉例:
fl 為 0........01:表示只有 fl 為 0 中存在有 sl 存在 free block。
sl[0] 為 0000000000000001 : 表示 fl 為 0 中 sl 為 0 對應的 free block。
fl 為 0......0101:表示只有 fl 為 0 或 2 中存在有 sl 存在 free block。
sl[0] 為 0000000000000011 : 表示 fl 為 0 中 sl 為 0 或 1 存在 free block。
```
#### block[_TLSF_FL_COUNT][_TLSF_SL_COUNT]
用來儲存每一個 fl sl 的 free list head。
```
舉例:
block[0][0] 為某個 ptr,則代表 fl 0 sl 0 的 free list 的第一個 block 位址為 ptr。
block[1][1] 為 NULL,則代表 fl 0 sl 0 的 free list 的為 empty。
```
用下圖來理解 tlsf_t 在 mem 中的儲存位置,詳細圖見上段 TLSF 架構:
```
+------------+ \
| |
| tlsf_t | ------------------> 函 bitmap blocks 等。
| |
+------------+ /
| block |
| |
+------------+
| free |
+------------+
| block |
+------------+
```
### mapping
如同上段 TLSF 架構所解釋,mapping 技術就是把 size 轉換為對應的 fl 以及 sl 值從而找到儲存在 `blocks[fl][sl]` 中最合適的 free list。
公式取自〈[TLSF: a New Dynamic Memory Allocator for Real-Time Systems](http://www.gii.upv.es/tlsf/files/ecrts04_tlsf.pdf)〉如下:
![](https://hackmd.io/_uploads/HJXxkB98h.png)
fl 很好理解,sl 的概念為 $size - 2^f$ 表示 size > 此層 fl 多少空間,$size - 2^f$ 除此 fl 層的 sl 大小為何即可得到對應的 sl。
$2^{SLI}$ 就是 `SL_COUNT`,$2^{f+1}-2^f/2^{SLI}$ 意思就是 fl 層的 sl 大小,$2^{f+1}-2^f/2^{SLI}$ 可化簡為 $\dfrac{2^f}{2^{SLI}}$,$size - 2^f$ 除 $2^f/2^{SLI}$ 則可表示為 $(size - 2^f) \times (\dfrac{2^{SLI}}{2^f})$。
下面程式碼只是將這個數學是換成用 bitwise 操作,以增加性能,解釋如下:
```c
INLINE void mapping(size_t size, uint32_t *fl, uint32_t *sl)
{
/*原因如同 block 資料結構中的解釋,且不會滿足上述 fl 的公式*/
if (size < BLOCK_SIZE_SMALL) {
/*太小的 block 會全部放入 fl 0 */
*fl = 0;
/*跟上述公式一模一樣,只是第一個 2^f 因為是起點所以為 0 */
/*第二個 2^f 的數學意義是此層 fl 的大小所以為 BLOCK_SIZE_SMALL*/
*sl = (uint32_t) size / (BLOCK_SIZE_SMALL / SL_COUNT);
} else {
uint32_t t = log2floor(size);
/*與公式相同*/
/*XOR 前可轉換成 size * 2^t / SL_COUNT */
/*XOR 代表減法,SL_COUNT 是把公式括號內的 2^f 提出運算之結果*/
*sl = (uint32_t) (size >> (t - SL_SHIFT)) ^ SL_COUNT;
/*原因如同上段 重要參數 解釋 FL_COUNT 一樣*/
*fl = t - FL_SHIFT + 1;
}
ASSERT(*fl < FL_COUNT, "wrong first level");
ASSERT(*sl < SL_COUNT, "wrong second level");
}
```
### insert remove free block 操作
#### block_remove block_insert
我們真實在 remove 以及 insert 時,只會知道要處理的 block 的位址,所以要先通過 mapping 尋找此大小的 block 對應的 fl sl,才會知道它存在哪個 free list (blocks[fl][sl])。在進行對於 list 的 node 操作。
```c
/* Remove a given block from the free list. */
INLINE void block_remove(tlsf_t *t, tlsf_block_t *block)
{
uint32_t fl, sl;
mapping(block_size(block), &fl, &sl);
remove_free_block(t, block, fl, sl);
}
/* Insert a given block into the free list. */
INLINE void block_insert(tlsf_t *t, tlsf_block_t *block)
{
uint32_t fl, sl;
mapping(block_size(block), &fl, &sl);
insert_free_block(t, block, fl, sl);
}
```
#### insert remove block in free list
對於已知 free list (blocks[fl][sl]),這兩個函式是把 block 給在對應的 free list (blocks[fl][sl]) 插入或是刪除,插入是直接插在 list 的最前端,作法就是基本的 linked list 操作,比較特別的是最後會去檢查 bitmap 需不需要更新。
```c
/* Remove a free block from the free list. */
INLINE void remove_free_block(tlsf_t *t,
tlsf_block_t *block,
uint32_t fl,
uint32_t sl)
{
ASSERT(fl < FL_COUNT, "wrong first level");
ASSERT(sl < SL_COUNT, "wrong second level");
tlsf_block_t *prev = block->prev_free;
tlsf_block_t *next = block->next_free;
if (next)
next->prev_free = prev;
if (prev)
prev->next_free = next;
/* If this block is the head of the free list, set new head. */
if (t->block[fl][sl] == block) {
t->block[fl][sl] = next;
/* If the new head is null, clear the bitmap. */
if (!next) {
t->sl[fl] &= ~(1U << sl);
/* If the second bitmap is now empty, clear the fl bitmap. */
if (!t->sl[fl])
t->fl &= ~(1U << fl);
}
}
}
/* Insert a free block into the free block list and mark the bitmaps. */
INLINE void insert_free_block(tlsf_t *t,
tlsf_block_t *block,
uint32_t fl,
uint32_t sl)
{
tlsf_block_t *current = t->block[fl][sl];
ASSERT(block, "cannot insert a null entry into the free list");
block->next_free = current;
block->prev_free = 0;
if (current)
current->prev_free = block;
t->block[fl][sl] = block;
t->fl |= 1U << fl;
t->sl[fl] |= 1U << sl;
}
```
### block 切割實作
這邊講解了 block 的各種切割實作,為甚麼需要切割在後續講解 `alloc` `free` 時會提到,或是直接參考[測驗五第一題](https://hackmd.io/@sysprog/linux2023-quiz5)。
#### block_can_split
檢查的 block 是否能分割 size 出來, 當然 block size 要大於等於 size ,加上 `sizeof(tlsf_block)
`要確保底部實體上下一個 block 的 prev 有地方儲存。
見下圖,取自 [esp-idf 的記憶體管理: TLSF 演算法](https://blog.csdn.net/gzxb1995/article/details/124504705)。
<img src="https://hackmd.io/_uploads/ByoT05SI3.png" width="50%" height="50%">
```c
INLINE bool block_can_split(tlsf_block_t *block, size_t size)
{
return block_size(block) >= sizeof(tlsf_block_t) + size;
}
```
#### block_rtrim_free block_rtrim_used block_ltrim_free
有三種不同的切割方式,取決於切割前 block 的狀態以及切割後想要 block 的狀態。
```c
/*把 free block 的頭部分割出指定的大小*/
/*尾部會剩下一個 free block*/
/*將分割完頭部的 free block 插入對應大小的 free list(blocks[fl][sl])*/
INLINE void block_rtrim_free(tlsf_t *t, tlsf_block_t *block, size_t size)
{
ASSERT(block_is_free(block), "block must be free");
/*先確定可否 split*/
if (!block_can_split(block, size))
return;
/*將一個 block 分割成兩塊,函式實作見後面*/
/*rest 代表尾部的 block*/
/*block 代表頭部的 block*/
tlsf_block_t *rest = block_split(block, size);
/*頭部的 block 因為後面的 block 變為新的 block*/
/*需要重新 link*/
block_link_next(block);
/*block 是 free 所以 rest 的 prev free 要設定為 true*/
block_set_prev_free(rest, true);
/*將分割完頭部的 free block 插入對應大小的 free list(blocks[fl][sl])*/
block_insert(t, rest);
}
/*把 use block 的頭部分割出指定的大小*/
/* 尾部會剩下一個 free block*/
/*將分割完尾部的 free block 插入對應大小的 free list(blocks[fl][sl])*/
INLINE void block_rtrim_used(tlsf_t *t, tlsf_block_t *block, size_t size)
{
ASSERT(!block_is_free(block), "block must be used");
/*先確定可否 split*/
if (!block_can_split(block, size))
return;
/*將一個 block 分割成兩塊,函式實作見後面*/
/*rest 代表尾部的 block*/
/*block 代表頭部的 block*/
tlsf_block_t *rest = block_split(block, size);
/*物理上前一個是 free block 才會在 merge 使用到 prev */
/*所以不需要 block_link_next(block) 來更新 next->prev*/
/*block 是 use 所以 rest 的 prev free 要設定為 false*/
block_set_prev_free(rest, false);
/*free block 與後面的 free block 合併(若存在)*/
/*在block_rtrim_free沒有這段,因為 free block 後不可能是 free block*/
/*在介紹到 free 實作時會說明這點*/
rest = block_merge_next(t, rest);
/*將分割完尾部的 free block 插入對應大小的 free list(blocks[fl][sl])*/
block_insert(t, rest);
}
/*把 free block 的頭部分割出指定的大小*/
/* 尾部會剩下一個 free block*/
/*將分割完頭部的 free block 插入對應大小的 free list(blocks[fl][sl])*/
/*return 尾部的 free block*/
/*使用於 aalloc*/
INLINE tlsf_block_t *block_ltrim_free(tlsf_t *t,
tlsf_block_t *block,
size_t size)
{
ASSERT(block_is_free(block), "block must be free");
ASSERT(block_can_split(block, size), "block is too small");
/*將一個 block 分割成兩塊,函式實作見後面*/
/*rest 代表尾部的 block*/
/*block 代表頭部的 block*/
/*BLOCK_OVERHEAD 是考量到 header*/
tlsf_block_t *rest = block_split(block, size - BLOCK_OVERHEAD);
/*block 是 free 所以 rest 的 prev free 要設定為 true*/
block_set_prev_free(rest, true);
/*頭部的 block 因為後面的 block 變為新的 block*/
/*需要重新 link*/
block_link_next(block);
/*將分割完頭部的 free block 插入對應大小的 free list(blocks[fl][sl])*/
block_insert(t, block);
/*return 尾部的 free block*/
return rest;
}
```
#### block_merge_prev block_merge_next
合併方式有兩種,注意這邊的合併是查找**物理上**的 block 來做合併,因為實際使用式合併 free block 所以一定要是 free block 才能實行此函式。
```c
/* 把前此 block 跟物理上前面的 block 合併,注意結果不會加到 free list
+------------+-------+
| free block | block |
+------------+-------+
*/
INLINE tlsf_block_t *block_merge_prev(tlsf_t *t, tlsf_block_t *block)
{
/*因為是用在 free block 合併,前一個一定要是 free block*/
if (block_is_prev_free(block)) {
tlsf_block_t *prev = block_prev(block);
ASSERT(prev, "prev block can't be null");
ASSERT(block_is_free(prev),
"prev block is not free though marked as such");
/*移除 prev 於 free list 中*/
block_remove(t, prev);
/*將前後合併*/
block = block_absorb(prev, block);
}
return block;
}
/* 把前此 block 跟物理上後面的 block 合併,注意結果不會加到 free list
+-------+------------+
| block | free block |
+-------+------------+
*/
INLINE tlsf_block_t *block_merge_next(tlsf_t *t, tlsf_block_t *block)
{
tlsf_block_t *next = block_next(block);
ASSERT(next, "next block can't be null");
if (block_is_free(next)) {
ASSERT(block_size(block), "previous block can't be last");
/*移除 next 於 free list 中*/
block_remove(t, next);
/*將前後合併*/
block = block_absorb(block, next);
}
return block;
}
```
#### block_split block_absorb
用來實現切割與合併。
```c
/*將給定 block 切 size 出來,並把剩下的空間定義一個新 block*/
/*並回傳 rest,注意這邊並未把 rest 放入 free list
+-----------+ <- block +-----------+ <- block
| prev | | prev |
+-----------+ +-----------+
| header | | header |
+-----------+ +-----------+ \
| next_free | | next_free |
+-----------+ +-----------+
| prev_free | | prev_free |
+-----------+ +-----------+ size
| ... | | ... |
| | before-------->after +-----------+ <- rest
| | | prev |
| | +-----------+ /
| | | header |
| | +-----------+
| | | next_free |
| | +-----------+
| | | prev_free |
| | +-----------+
| | | ... |
+-----------+ +-----------+
*/
INLINE tlsf_block_t *block_split(tlsf_block_t *block, size_t size)
{
/*扣掉的 BLOCK_OVERHEAD 是因為 rest 要指在 prev*/
tlsf_block_t *rest = to_block(block_payload(block) + size - BLOCK_OVERHEAD);
/*扣掉的 BLOCK_OVERHEAD 是因為要分 header 出來,header 不算在 size*/
size_t rest_size = block_size(block) - (size + BLOCK_OVERHEAD);
ASSERT(block_size(block) == rest_size + size + BLOCK_OVERHEAD,
"rest block size is wrong");
ASSERT(rest_size >= BLOCK_SIZE_MIN, "block split with invalid size");
/*新 block size 設定*/
rest->header = rest_size;
ASSERT(!(rest_size % ALIGN_SIZE), "invalid block size");
/*更新切下來以後的空間以及原本的 block*/
block_set_free(rest, true);
block_set_size(block, size);
return rest;
}
/* 合併前後 block,前 prev 後 block,示意圖為 split 的反向
+-----------+ <- prev +-----------+ <- prev
| prev | | prev |
+-----------+ +-----------+
| header | | header |
+-----------+ +-----------+
| next_free | | next_free |
+-----------+ +-----------+
| prev_free | | prev_free |
+-----------+ +-----------+
| ... | | ... |
| | after<--------before +-----------+ <- block
| | | prev |
| | +-----------+
| | | header |
| | +-----------+
| | | next_free |
| | +-----------+
| | | prev_free |
| | +-----------+
| | | ... |
+-----------+ +-----------+
*/
INLINE tlsf_block_t *block_absorb(tlsf_block_t *prev, tlsf_block_t *block)
{
ASSERT(block_size(prev), "previous block can't be last");
/*加上的 BLOCK_OVERHEAD 是因為要加 header 回來,header 不算原本的在 size*/
prev->header += block_size(block) + BLOCK_OVERHEAD;
block_link_next(prev);
return prev;
}
```
### tlsf init
在 [jserv/tlsf](https://github.com/jserv/tlsf-bsd) 中並未找到關於 tlsf pool 的初始化,但初始化的狀態可以大大幫助理解程式碼,這邊先使用 [esp-idf 的記憶體管理: TLSF 演算法](https://blog.csdn.net/gzxb1995/article/details/124504705) 這篇文章的概念,見下圖。
這是一個 TLSF 初始化完的狀態,有幾個重點:
1. tlsf_t 會分配在記憶體最上方的位置。
2. 一開始會將整個 memory pool 初始化為一個 free block,換句話說指會有一個 blocks[fl][sl] 有指向 free list (只有一個 free list)。
所以不難想像,一開始無論 alloc 的 size `mapping` 後的 fl sl 為何,在 `block_find_suitable` 後都會找到那個唯一有 free block 的 fl sl index。
![](https://hackmd.io/_uploads/SkT54msI2.png)
[jserv/tlsf](https://github.com/jserv/tlsf-bsd) 並未有 init 函式 而是使用 `arena_grow` `arena_shrink` 代替。
```c
static bool arena_grow(tlsf_t *t, size_t size)
{
//找到新的 size
size_t req_size =
(t->size ? t->size + BLOCK_OVERHEAD : 2 * BLOCK_OVERHEAD) + size;
//可不可以給我這個 size
void *addr = tlsf_resize(t, req_size);
if (!addr)
return false;
ASSERT((size_t) addr % ALIGN_SIZE == 0, "wrong heap alignment address");
//找到增加空間後最新 block 的位置
tlsf_block_t *block =
to_block(t->size ? (char *) addr + t->size - 2 * BLOCK_OVERHEAD
: (char *) addr - BLOCK_OVERHEAD);
if (!t->size)
block->header = 0;
check_sentinel(block);
//設定新得到的 block
block->header |= size | BLOCK_BIT_FREE;
block = block_merge_prev(t, block);
block_insert(t, block);
//設定新的 sentinel
tlsf_block_t *sentinel = block_link_next(block);
sentinel->header = BLOCK_BIT_PREV_FREE;
t->size = req_size;
check_sentinel(sentinel);
return true;
}
static void arena_shrink(tlsf_t *t, tlsf_block_t *block)
{
//新的 free block 是最尾(下一個是 sentinel)就把空間還回去
check_sentinel(block_next(block));
size_t size = block_size(block);
ASSERT(t->size + BLOCK_OVERHEAD >= size, "invalid heap size before shrink");
//刪掉 free block 的 size
t->size = t->size - size - BLOCK_OVERHEAD;
//只剩 sentinel
if (t->size == BLOCK_OVERHEAD)
t->size = 0;
tlsf_resize(t, t->size);
//還有其他 block,設定新 sentinel
if (t->size) {
block->header = 0;
check_sentinel(block);
}
}
```
### malloc
討論完 init 後,我們來看如何 malloc,具體流程如下:
1. 先把 size 對齊,如果每個申請的 size 都申請 `ALIGN_SIZE` 的倍數,自然他們的 ptr 就會是對齊好的。
2. 找到此 size 最合適的 free list。
3. 從此 free list 中取出一個 free block 作為 alloc 的對象。
4. 將此 free block 從 list 中移除。
5. 將此 free block 切出 size 作為 use block。
6. 將切剩的 free block 放到對應合適大小的 free list。
`adjust_size`:1
`block_find_free`:2~4
`block_use`:5~6
```c
void *tlsf_malloc(tlsf_t *t, size_t size)
{
size = adjust_size(size, ALIGN_SIZE);
if (UNLIKELY(size > TLSF_MAX_SIZE))
return NULL;
tlsf_block_t *block = block_find_free(t, size);
if (UNLIKELY(!block))
return NULL;
return block_use(t, block, size);
}
```
接下來就可以看這些函式具體的實作:
#### adjust_size
呼叫 `align` 等對其函式,若對齊後比最小可以存在的 BLOCK 還小,就回傳`BLOCK_SIZE_MIN`。
```c
/* Adjust allocation size to be aligned, and no smaller than internal minimum.
*/
INLINE size_t adjust_size(size_t size, size_t align)
{
size = align_up(size, align);
return size < BLOCK_SIZE_MIN ? BLOCK_SIZE_MIN : size;
}
```
##### align
對齊的詳細概念以及意義見[你所不知道的 C 語言:記憶體管理、對齊及硬體特性篇](https://hackmd.io/@sysprog/c-memory)。 `align_ptr` 會呼叫 `align_up` 得到向上對齊後的位址 (例如 15 對齊 8 得到 16),即找向上最接近 align 的倍數。因為 size_t align 會是二的次方,`align - 1` 後會是 0+1+ 的形式,做 | 的意義在於將 x 對 align 取餘數後為 align - 1,最後的 `+1` 自然就會得到向上最接近 align 的倍數,一開始做 `x-1` 是確保邊界狀況 (例如 16 對齊 8,所以先把 16 改成 15) 也會成立。
```c
INLINE size_t align_up(size_t x, size_t align)
{
ASSERT(!(align & (align - 1)), "must align to a power of two");
return (((x - 1) | (align - 1)) + 1);
}
INLINE char *align_ptr(char *p, size_t align)
{
return (char *) align_up((size_t) p, align);
}
```
#### `block_find_free`
總體用途上面已經解釋過了。
這邊需要特別提的是 `提級申請` 概念,就是會用高一層的 size,去做 mapping。舉例:原本 size 是落在 `blocks[1][1]`,但我們 mapping 完想要的 fl sl 反而是 `blocks[1][2]`。
原因很好理解,假如我們使用原本的 size 介於:
$[ 2^f, 2^f + N)$
`mapping` 後得到的 fl sl 的 free block 空間大小也會介於:
$[ 2^f, 2^f + N)$
所以找到的 free block 不一定比 size 大,遍例整個 free list 會浪費時間成本,所以我們做提級,確保找到的 free block 一定大於 size。
我們希望 `mapping` 後得到的 fl sl 的 free block 空間大小會介於:
$[ 2^f + N, 2^f + 2N)$
`round_block_size` 就是先把原本的 size 提到下一層的大小,讓我們最後得到的 fl sl 會提級。
```c
INLINE tlsf_block_t *block_find_free(tlsf_t *t, size_t size)
{
size_t rounded = round_block_size(size);
uint32_t fl, sl;
/*找到此 size 最合適的 free list*/
mapping(rounded, &fl, &sl);
tlsf_block_t *block = block_find_suitable(t, &fl, &sl);
if (UNLIKELY(!block)) {
if (!arena_grow(t, rounded))
return NULL;
block = block_find_suitable(t, &fl, &sl);
ASSERT(block, "no block found");
}
ASSERT(block_size(block) >= size, "insufficient block size");
/*從此 free list 中取出一個 free block 作為 alloc 的對象*/
/*將此 free block 從 list 中移除*/
remove_free_block(t, block, fl, sl);
return block;
}
```
##### round_block_size
$((size_t) 1 << (log2floor(size) - SL_SHIFT))$ 等價於,$2^f/SL\_COUNT$ 解釋 ,`mapping` 時講過了,就是這個 size 所屬 SL 層的大小。
$size + t$ 原本的 `size` 加上所屬 SL 層的大小,一定會變到下一個 SL 層區間。
`& ~t` 代表加上後多出來的餘數就算了。
```c
/* Round up to the next block size */
INLINE size_t round_block_size(size_t size)
{
size_t t = ((size_t) 1 << (log2floor(size) - SL_SHIFT)) - 1;
return size >= BLOCK_SIZE_SMALL ? (size + t) & ~t : size;
}
```
##### block_find_suitable
就算找到了儲存了最合適大小的 free block 的 free list 所對應的 fl sl,此 fl sl (blocks[fl][sl]) 也不一定有 node 在 free list 內,意即沒有此大小區間的 free block 存在 memeory 。這時就必須往找尋更大的 free block (更大的 fl sl),來做為未來 alloc 的位址。
這邊也是使用 bitwise 操作,使得效率加快,都蠻容易理解的。
在查找時直接 bitmap ,不用使用遍例找到最合適的 fl sl ,所以搜尋時間為 O(1)。
```c
INLINE tlsf_block_t *block_find_suitable(tlsf_t *t, uint32_t *fl, uint32_t *sl)
{
ASSERT(*fl < FL_COUNT, "wrong first level");
ASSERT(*sl < SL_COUNT, "wrong second level");
/* 從 sl 的 bitmap 中將此 sl 對應的 bit 及更高位的 bit 都取出來 */
uint32_t sl_map = t->sl[*fl] & (~0U << *sl);
/*如果結果為 0,代表此 fl 沒有相應大小的 free block 了*/
if (!sl_map) {
/*去更大的 fl 尋找*/
/*取出 fl bitmap 中更高位的 bit*/
uint32_t fl_map = t->fl & (uint32_t) (~(uint64_t) 0 << (*fl + 1));
/*如果結果為 0,代表整個 memory pool 都沒有相應大小的 free block 了*/
if (UNLIKELY(!fl_map))
return NULL;
/*若不為 0,就從剛剛取出的更高位的 bit 中找最小那位*/
*fl = bitmap_ffs(fl_map);
ASSERT(*fl < FL_COUNT, "wrong first level");
/*取出此區間的 sl bitmap*/
sl_map = t->sl[*fl];
ASSERT(sl_map, "second level bitmap is null");
}
/*從 sl_map 中取出最低位的 bit 作為我們最後的 sl 層*/
/*因為確認過 fl bitmap 以及 sl bitmap,現在的 sl_map 一定不為 0*/
*sl = bitmap_ffs(sl_map);
ASSERT(*sl < SL_COUNT, "wrong second level");
return t->block[*fl][*sl];
}
```
#### block_use
作用如上面解釋,各個函式實作上面都介紹過了。
```c
INLINE void *block_use(tlsf_t *t, tlsf_block_t *block, size_t size)
{
/*將此 free block 切出 size*/
/*將切剩的 free block 放到對應合適大小的 free list*/
block_rtrim_free(t, block, size);
/*切出來的作為 use block*/
block_set_free(block, false);
/*malloc 最後回傳給 usr 是回傳 payload 的 ptr*/
return block_payload(block);
}
```
### free
在進到 free 前先講解 free 的基本運行流程,概念來自對[測驗五第一題](https://hackmd.io/@sysprog/linux2023-quiz5)的理解。
在記憶體管理中,當一個 block 要釋放時,我們會去檢查其物理上前後是否為 free block ,若為 free block 我們會將釋放以後的 block 跟前後的 free block 作合併,得到一個更大的 free block ,這樣我們未來在做 alloc 時就有一個更大的連續空間可以使用。
`merge` 的流程如下:
```
+------------+-------+------------+
| free block | block | free block |
+------------+-------+------------+
在 tlsf 的實作中,會先去跟前面的 free block 做 merge
+--------------------+------------+
| free block | free block |
+--------------------+------------+
然後再跟後面的 free block 做 merge 最後得到最大的 free block
+---------------------------------+
| free block |
+---------------------------------+
如果要釋放的 block 前或後不為 free block 怎麼辦?
這不用擔心,因為 merge_pre 跟 merge_next 在執行前都會先去檢查 pre 或 next
是不是 free block ,若不是 merge 將甚麼都不會做,最後還是會得到最大的 free block
```
我們每次 free 後,都會去做合併,所以在 memory pool 中,是不會存在兩個 free block 連在一起的狀況。
這邊要特別注意的是在 tlsf 在合併後,因為 free block 的大小改變了,所以要重新做 mapping 得到對應的 fl sl ,並將新的 free block 放入對應的 free list (blocks[fl][sl])。
```c
void tlsf_free(tlsf_t *t, void *mem)
{
if (UNLIKELY(!mem))
return;
/*usr 只會知道 payload 的位址*/
/*所以傳進來後,要先推回 block 的位址*/
tlsf_block_t *block = block_from_payload(mem);
ASSERT(!block_is_free(block), "block already marked as free");
/*把此 block 改為 free*/
block_set_free(block, true);
/*先跟前面做合併,若前面不為 free block 這個函式甚麼都不會做*/
block = block_merge_prev(t, block);
/*再跟後面做合併,若後面不為 free block 這個函式甚麼都不會做*/
block = block_merge_next(t, block);
if (UNLIKELY(!block_size(block_next(block))))
arena_shrink(t, block);
else
/*最後合併完成的新 free block 要放到對應的 free list (blocks[fl][sl])*/
block_insert(t, block);
}
```
最後,有一些比較延伸的函式還沒寫,未來有機會補上:
`tlsf_aalloc`、`tlsf_realloc`、`arena_shrink`、`arena_grow` 、`check_sentinel`、`block_rtrim_used`
## TLSF 效能分析與特性討論
> github [cin-cout/tlsf-bsd](https://github.com/cin-cout/tlsf-bsd)
### 前置設定
為了避免會影響的效能測量的因素,需要做前置設定。而每次開機都要重新輸入命令很不方便,所以將效能相關的前置設定與程式執行包成了一個 measure.sh 檔,細節參照 [fibdrv-開發紀錄](https://hackmd.io/uHe0dpXJRZ292NfIKWApag)。
```bash
#!/bin/bash
CPUID=5
ORIG_ASLR=`cat /proc/sys/kernel/randomize_va_space`
ORIG_GOV=`cat /sys/devices/system/cpu/cpu$CPUID/cpufreq/scaling_governor`
ORIG_TURBO=`cat /sys/devices/system/cpu/intel_pstate/no_turbo`
ORIG_SMT=`cat /sys/devices/system/cpu/smt/control`
sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"
sudo sh -c "echo performance > /sys/devices/system/cpu/cpu$CPUID/cpufreq/scaling_governor"
sudo sh -c "echo 1 > /sys/devices/system/cpu/intel_pstate/no_turbo"
sudo sh -c "echo off > /sys/devices/system/cpu/smt/control"
make run
sudo sh -c "echo $ORIG_ASLR > /proc/sys/kernel/randomize_va_space"
sudo sh -c "echo $ORIG_GOV > /sys/devices/system/cpu/cpu$CPUID/cpufreq/scaling_governor"
sudo sh -c "echo $ORIG_TURBO > /sys/devices/system/cpu/intel_pstate/no_turbo"
sudo sh -c "echo $ORIG_SMT > /sys/devices/system/cpu/smt/control"
```
### 實作 tlsf_init
在 [jserv/tlsf](https://github.com/jserv/tlsf-bsd) 中 tlsf 會在沒有合適的 free block 時動態的從 pre-allocating 的空間中拿取新的空間,但考慮到 pre-allocate 的空間在 tlsf 執行時是不會被 tlsf 外的資料所使用的,放著也只是閒置著,所以可以在 tlsf init 時就把所有 pre-allocte 的空間放入 tlsf 內,可以減少因不斷動態增加空間的成本。
想法參考 [mattconte/tlsf](https://github.com/mattconte/tlsf/blob/master/tlsf.c) 實作了另一種 tlsf init 的方式:
```c
static size_t align_down(size_t x, size_t align)
{
ASSERT(0 == (align & (align - 1)), "must align to a power of two");
return x - (x & (align - 1));
}
bool tlsf_add_pool(tlsf_t *t, void* mem, size_t mem_size)
{
tlsf_block_t * block;
tlsf_block_t * next;
const size_t pool_bytes = align_down(mem_size - 2*BLOCK_OVERHEAD, ALIGN_SIZE);
ASSERT((size_t) mem % ALIGN_SIZE == 0, "wrong heap alignment address");
block = to_block((char *) mem - BLOCK_OVERHEAD);
block->header |= pool_bytes | BLOCK_BIT_FREE;
block_insert(t, block);
next = block_link_next(block);
next->header = BLOCK_BIT_PREV_FREE;
t->size = mem_size;
return true;
}
void tlsf_init(tlsf_t *t, void* mem, size_t mem_size)
{
*t = TLSF_INIT;
tlsf_add_pool(t, mem, mem_size);
}
```
#### tlsf_init 效能比較
隨後對於原本的方法以及新的方法做比較。
x : 分配的 block size 大小(範圍),假如 x 為 2000,分配的 block min 為 2000,block max 為 2000 + 64,單位為 bytes 。
y : 平均 malloc + free 的時間,單位為 us。
藍色為使用 [jserv/tlsf](https://github.com/jserv/tlsf-bsd) arena grow/shrink 動態獲取 free block 的方法。
橘色為使用調整後的 tlsf_init 方法。
![](https://hackmd.io/_uploads/rJO9bwlv3.png)
可以看出因為少了不斷動態增加空間的成本, tlsf_init 的方式效能有不小的提昇。而無論是哪種實作方式,平均 alloc 的 size 越大,所花的時間也會有略為的上升。
#### tlsf_init 效能比較(大範圍)
因為上張圖,每一項測資範圍區間僅有 64 bytes ,為了確認當申請的 block 範圍大一點時,是否有一樣的現象,所以又進行了測試。
x : 執行的 50 次相同的設定。
y : 平均 malloc + free 的時間,單位為 us。
左圖為使用 [jserv/tlsf](https://github.com/jserv/tlsf-bsd) arena grow/shrink 動態獲取 free block 的方法。
右圖為使用調整後的 tlsf_init 方法。
不同線代表不同 size range。
<div style="display: flex;">
<img src="https://hackmd.io/_uploads/SyVukDxP2.png" width="50%" height="50%">
<img src="https://hackmd.io/_uploads/HJEuJvgDn.png" width="50%" height="50%">
</div>
結果與上一種測試相同, tlsf_init 的方式效能有不小的提昇。而無論是哪種實作方式,平均 alloc 的 size 越大,所花的時間也會有略為的上升。
另外可以從這個測試看出,因為在測試時 block size 是 random 的,使用 [jserv/tlsf](https://github.com/jserv/tlsf-bsd) arena grow/shrink 動態獲取 free block 的方法,會因為每次測試 alloc 的 size 大小順序具有隨機性,所以會比較不穩定,較難預期結果。
### TLSF 特性討論
對 tlsf pre-allocating 以及 O(1) 的特性做討論。
#### pre-allocating
下圖物理性質見上段。
這邊可以看到在約接近 10000 ~ 10000 + 64 的 size 後出現了不如預期的結果。**這是因為在 tlsf pre-allocating 階段給的空間不足**,導致後期在測量大的 size 時, alloc 會失敗(回傳 NULL),也因為這樣不須做後續的其他步驟,時間自然降下來。
所以在使用 tlsf 時要算清楚應事先分配多少空間,這個空間分配給 tlsf 後就不會給其他使用,分配太大可能會浪費記憶體的空間,太少可能會造成 tlsf alloc 時無空間可使用。
還有一點是 tlsf 在 alloc 時除了原本要求的 size ,還會需要一個 word 去除存 block->header,加上在使用 tlsf 的過程可能有碎片化的產生,剩餘的 free block 不連續,所以在 pre-allocating 時不能只分配剛剛好大小的。
![](https://hackmd.io/_uploads/SkS57tkw3.png)
#### O(1)
TLSF 的實作上沒有使用到遍歷,所以確實是 O(1),但是從下圖可知,在要求的 size 越大時,雖然時間成長的幅度很小,需要的時間也會更多,這是因為更大 block alloc 可能會更容易觸發到一些 if 的條件,進而花費更多時間。
結論是,雖然 TLSF 在演算法的定義中是 O(1) ,但是當 alloc size 越大時,運行開銷更大的趨勢。
![](https://hackmd.io/_uploads/rJO9bwlv3.png)
## xvmalloc 架構
如同 [xvMalloc](https://code.google.com/archive/p/compcache/wikis/xvMalloc.wiki) 所敘述,xvmalloc 是基於 TLSF 的記憶體配置器,大部分實作與 TLSF 相同,也使用二層索引的方式尋找最合適的 free list 而得到 free block , block 方面也如同 TLSF 設計,有紀錄物理上前一個 block 的位置的 ptr 和前後 free block 的 ptr,與 TLSF 不同在於:
1. 考量到硬體的架構, xvmalloc 在 malloc 的時候會以 page 為單位,以減少在真正使用空間時,一個 data 橫跨兩個 page 增加 page fault 的機會。
2. 程式碼實作中有 lock 的機制,所以可以運行於多平行系統中。
## xvmalloc 實作解析
程式碼的部分參照 [linux/commit](https://github.com/torvalds/linux/commit/644bf7b5983cf2540b57a5b25b775cb3c1e8e943)。大部分與 TLSF 相同,此處特別講解考量 page 後的實作差異。
### 資料結構
`xv_pool` 等同於 `tlsf_t` 只是 free list 不是紀錄 block 的 ptr 而是 page 與 offset,page 以及 offset 意義後續會提到。
```c
struct freelist_entry {
struct page *page;
u16 offset;
u16 pad;
};
struct xv_pool {
ulong flbitmap;
ulong slbitmap[MAX_FLI];
spinlock_t lock;
struct freelist_entry freelist[NUM_FREE_LISTS];
/* stats */
u64 total_pages;
};
```
`block` 也與 TLSF 相同只是改為 page 和 offset。
```c
struct link_free {
struct page *prev_page;
struct page *next_page;
u16 prev_offset;
u16 next_offset;
};
struct block_header {
union {
/* This common header must be ALIGN bytes */
u8 common[XV_ALIGN];
struct {
u16 size;
u16 prev;
};
};
struct link_free link;
};
```
### 考量 page 實作後的程式碼
#### grow shrink pool
與 TLSF 的 `aerna_grow` 類似,在空間不足時會自動去增加空間。
`page = alloc_page(flags);` 可知,每次增加的空間就是一個 page 。
`insert_block(pool, page, 0, block);`會直接將這個 page 當成 free block 放入對應大小的 free list 中,由此可知對大的 free block 不會超過 `PAGE_SIZE`,所以不能 allocate 超過 `PAGE_SIZE` 的大小。
`get_ptr_atomic` 是將 page 映射到 process 的虛擬空間,對這個虛擬空間作操作才可以更改 page 儲存的內容。
`put_ptr_atomic` 使用完後要將其解除映射,無法同時映射多個 page。
```c
static int grow_pool(struct xv_pool *pool, gfp_t flags)
{
struct page *page;
struct block_header *block;
page = alloc_page(flags);
if (unlikely(!page))
return -ENOMEM;
stat_inc(&pool->total_pages);
spin_lock(&pool->lock);
block = get_ptr_atomic(page, 0, KM_USER0);
block->size = PAGE_SIZE - XV_ALIGN;
set_flag(block, BLOCK_FREE);
clear_flag(block, PREV_FREE);
set_blockprev(block, 0);
insert_block(pool, page, 0, block);
put_ptr_atomic(block, KM_USER0);
spin_unlock(&pool->lock);
return 0;
}
```
在 `xv_free` 有關於 shrink 的函式,就是如果 free 完後這個 page 沒有儲存任何資料,就將其釋放。
```c
/* No used objects in this page. Free it. */
if (block->size == PAGE_SIZE - XV_ALIGN) {
put_ptr_atomic(page_start, KM_USER0);
spin_unlock(&pool->lock);
__free_page(page);
stat_dec(&pool->total_pages);
return;
}
```
xvmalloc 的 grow 跟 shrink 比起 TLSF 更具有意義, TLSF 是在已經 pre-allocating 好的空間中做動態增加或減少空間,但是 pre-alocate 的空間就算沒有在 TLSF 中使用,也不會作為其他用途。而 xvmalloc 則是動態增加或減少 page ,沒在 xvmalloc 中使用的 page 是可以作為其他用途的。
### malloc free
最後看 `xv_malloc` 以及 `xv_free`。
大致上與 TLSF 相同,只是加入了 page 的限制。每次 malloc 的空間都一定會是在同一個 page,一個 block 只會在一個 page 中,一個 page 中可能被切成多個 block,不會存在一個 block 跨越兩個 page。offset 就是用來記錄 page 內的位置
#### malloc
`xv_malloc` 一樣會使用 mapping 的方式找到對應的 fl sl,並將其分配出去,然後再將這個 page 剩下還沒被使用的空間(用 offset 紀錄)放到對應的 fl sl,所以一個 freelist 儲存的 free block 可能是不同 page 的剩餘空間。
#### free
在 free 一塊空間時,會去看此空間所屬的 page 在此空間( offset )前後是否也沒被使用,若沒被使用,則合併,再將合併後的 block 儲存到對應的 free list。
## zsmalloc 架構
:::warning
:warning: 應闡述 zsmalloc 到底在解決什麼問題?開發動機是什麼?而非急著閱讀原始程式碼
:notes: jserv
:::
### 參考資料
程式碼:[zsmalloc.c](https://elixir.bootlin.com/linux/latest/source/mm/zsmalloc.c)
zsmalloc 的程式碼解析在網路上有很多了,所以這邊不特別對程式碼去做講解,只對基本架構去做解釋,有基本的的 allocator 知識再加上網路上的文章可以很快速的理解,以下是有用的網路文章:
1. [記憶體管理特性分析(一):zsmalloc記憶體配置器分析](https://zhuanlan.zhihu.com/p/558763137)
程式碼乾淨整齊且有註解解釋,但解釋的很籠統。
2. [zsmalloc 配置器源码分析](https://toutiao.io/posts/1v6m59/preview)
解釋的較為詳細,但是程式碼排版不佳,圖片有些已經找不到了。
### 架構解析
zsmalloc 架構如下:
```
+-----------------------------------------+
|Zpool |
| +-----------------------------------+ |
| | zspage | |
| | +--------+ +--------+ | |
| | | object | | object | . . . | |
| | +--------+ +--------+ | |
| +-----------------------------------+ |
| |
| +-----------------------------------+ |
| | zspage | |
| +-----------------------------------+ |
| . |
| . |
| . |
| |
+-----------------------------------------+
```
如同上圖所示, zsmalloc 會有一個 pool ,裡面存著多個 zspage ,一個 zspage 由一致多個 page 組成,每個 zspage 裡面都會存放多個 object ,每一個 zspage 的 object size 有可能不同,有大有小,但是同一個 zspage 中 object size 都是一樣的, object size 不會超過 page size (不是 zspage size),一個 object 相當於一個 block ,我們一次 alloc 的資料只會分配一個大小相近的 object 。
> 注意, zsmalloc 不是基於 TLSF 的機制,所以不會有 allocate 完將一個 object 剩下的空間放到別的 freelist ,就是一個蘿菠一個坑,一次 allocate 分一個 object 出去, zpage 用完會在動態新增,也會有 zpage 全空就 shrink 的機制。
## [xvmalloc](https://github.com/torvalds/linux/commit/644bf7b5983cf2540b57a5b25b775cb3c1e8e943) [zsmalloc](https://www.kernel.org/doc/html/v5.0/vm/zsmalloc.html) 效能比較
> github [cin-cout/allocator_benchmark](https://github.com/cin-cout/allocator_benchmark)
### 前置設定
#### zsmalloc config 設定
1. 先確認 linux 版本
```shell
$ uname -a
```
2. 移動到存放 config 的地方,/usr/src/`<linux version>`
```shell
$ cd /usr/src/linux-headers-5.4.0-146-generic
```
3. 編輯 .config
```shell
把 .config 從唯讀換成可寫
$ sudo chmod +w .config
$ vim .config
```
4. 更改 zsmalloc 所需的 config
```shell
CONFIG_ZSMALLOC=y
CONFIG_ZPOOL=y
CONFIG_ZBUD=y
CONFIG_Z3FOLD=y
CONFIG_ZRAM=y
# optional
CONFIG_ZSMALLOC_STAT=y
```
改成 m 也可以,但就是要額外自己掛載 module,而不是開機自動掛載。
掛載方式如下:
```shell
zram 換成想掛載的 module 名稱
$ sudo modprobe zram
```
5. 重新開機
```shell
$ sudo reboot
```
### zsmalloc vmalloc 效能分析實現
因為 linux kernel 已經有 `<linux/zsmalloc.h>` 想先測試 zsmalloc 在 kernel 的效能,bench 程式參考 [tlsf-bsd](https://github.com/jserv/tlsf-bsd) 的 bench.c ,程式很直觀,不細作解釋,只是改寫了部份,改成 kernel 端執行,使用 character device driver ,架構參考 [sysprog21/fibdrv](https://github.com/sysprog21/fibdrv) 。
#### kernel
用 write 作為 usr 呼叫 kernel module 執行效能運算程式,用 client 傳入的 size 決定要測量哪種 allocator。
```c
static ssize_t bench_write(struct file *file,
const char *buf,
size_t size,
loff_t *offset)
{
ktime_t kt;
size_t num_blks = 10000;
size_t loops = 10000000;
static unsigned long handle_array[10000];
memset(handle_array, 0, sizeof(handle_array));
static struct page *page_array[10000];
memset(page_array, 0, sizeof(page_array));
static u32 offset_array[10000];
memset(offset_array, 0, sizeof(offset_array));
switch (size) {
case 0:
zpool = zs_create_pool("mypool");
kt = ktime_get();
run_zsmalloc_benchmark(loops, *offset, *offset+32, handle_array, num_blks);
kt = ktime_sub(ktime_get(), kt);
zs_destroy_pool(zpool);
break;
case 1:
xpool = xv_create_pool();
kt = ktime_get();
run_xvmalloc_benchmark(loops, *offset, *offset+32, page_array, offset_array, num_blks);
kt = ktime_sub(ktime_get(), kt);
xv_destroy_pool(xpool);
break;
}
return (ssize_t) ktime_to_ns(kt);
}
```
##### get_random_block_size
```c
static size_t get_random_block_size(size_t blk_min, size_t blk_max)
{
if (blk_max > blk_min)
return blk_min + ((size_t) prandom_u32() % (blk_max - blk_min));
return blk_min;
}
```
`zsmalloc` 與 `vmalloc` 的差別在於在呼叫空間後, `zsmalloc` 會回傳一個 `unsigned long` 的 handle 作為呼叫空間的代號,若要取得可使用的地址需要再做 mapping,`vmalloc` 內部則會先做 mapping,所以直接會回傳 `void *`。
##### run_xvmalloc_benchmark
```c
static void run_xvmalloc_benchmark(size_t loops,
size_t blk_min,
size_t blk_max,
struct page **page_array,
u32 *offset_array,
size_t num_blks)
{
while (loops--) {
size_t next_idx = (size_t) prandom_u32() % num_blks;
size_t blk_size = get_random_block_size(blk_min, blk_max);
if (page_array[next_idx]) {
xv_free(xpool, page_array[next_idx], offset_array[next_idx]);
/* Insert the newly alloced block into the array at a random
* point.
*/
xv_malloc(xpool, blk_size, &page_array[next_idx], &offset_array[next_idx], __GFP_ZERO);
} else {
/* Insert the newly alloced block into the array at a random point.
*/
xv_malloc(xpool, blk_size, &page_array[next_idx], &offset_array[next_idx], __GFP_ZERO);
}
}
/* Free up all allocated blocks. */
for (size_t i = 0; i < num_blks; i++) {
if (page_array[i])
xv_free(xpool, page_array[i], offset_array[i]);
}
}
```
##### run_zsmalloc_benchmark
```c
static void run_zsmalloc_benchmark(size_t loops,
size_t blk_min,
size_t blk_max,
unsigned long *handle_array,
size_t num_blks)
{
while (loops--) {
size_t next_idx = (size_t) prandom_u32() % num_blks;
size_t blk_size = get_random_block_size(blk_min, blk_max);
if (handle_array[next_idx]) {
zs_free(zpool, handle_array[next_idx]);
/* Insert the newly alloced block into the array at a random
* point.
*/
handle_array[next_idx] = zs_malloc(zpool, blk_size, GFP_KERNEL);
} else {
/* Insert the newly alloced block into the array at a random point.
*/
handle_array[next_idx] = zs_malloc(zpool, blk_size, GFP_KERNEL);
}
}
/* Free up all allocated blocks. */
for (size_t i = 0; i < num_blks; i++) {
if (handle_array[i]){
zs_free(zpool, handle_array[i]);
}
}
}
```
#### user space
呼叫用 wirte 呼叫 kernel module,用 `offset` 決定要測試效能的 block size 區間,舉例 `offset` 為 0,測試 block min 即為 0 ,block max 為 0+32, write 最後一個參數用來控制要測試何種 allocator , buf 目前沒有實質用途。
```c
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#define BENCH_DEV "/dev/bench"
#define MAX_OFFSET 16320
#define LOOPS 10000000
int main()
{
unsigned long long sz;
char write_buf[] = "testing writing";
int offset = MAX_OFFSET;
FILE *fptr_z;
FILE *fptr_v;
fptr_z = fopen("./txt/zsmalloc_bench.txt", "w");
fptr_v = fopen("./txt/xvmalloc_bench.txt", "w");
int fd = open(BENCH_DEV, O_RDWR);
if (fd < 0) {
perror("Failed to open character device");
exit(1);
}
for (int i = 0; i <= offset; i+=64) {
lseek(fd, i, SEEK_SET);
printf("now %d %d\n", i, i+64);
sz = write(fd, write_buf, 0);
printf("%d %d %.3f\n", i, i+64, sz * 1e-3/(double)LOOPS);
fprintf(fptr_v, "%d %d %.3f\n", i, i+64, sz * 1e-3/(double)LOOPS);
}
for (int i = 0; i <= offset; i+=64) {
lseek(fd, i, SEEK_SET);
printf("now %d %d\n", i, i+64);
sz = write(fd, write_buf, 1);
printf("%d %d %.3f\n", i, i+64, sz * 1e-3/(double)LOOPS);
fprintf(fptr_v, "%d %d %.3f\n", i, i+64, sz * 1e-3/(double)LOOPS);
}
fclose(fptr_z);
fclose(fptr_v);
close(fd);
return 0;
}
```
### 效能分析
#### 前置設定
為了避免會影響的效能測量的因素,需要做前置設定。而每次開機都要重新輸入命令很不方便,所以將效能相關的前置設定與程式執行包成了一個 measure.sh 檔,細節參照 [fibdrv-開發紀錄](https://hackmd.io/uHe0dpXJRZ292NfIKWApag)。
```bash
#!/bin/bash
CPUID=5
ORIG_ASLR=`cat /proc/sys/kernel/randomize_va_space`
ORIG_GOV=`cat /sys/devices/system/cpu/cpu$CPUID/cpufreq/scaling_governor`
ORIG_TURBO=`cat /sys/devices/system/cpu/intel_pstate/no_turbo`
ORIG_SMT=`cat /sys/devices/system/cpu/smt/control`
sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"
sudo sh -c "echo performance > /sys/devices/system/cpu/cpu$CPUID/cpufreq/scaling_governor"
sudo sh -c "echo 1 > /sys/devices/system/cpu/intel_pstate/no_turbo"
sudo sh -c "echo off > /sys/devices/system/cpu/smt/control"
make run
sudo sh -c "echo $ORIG_ASLR > /proc/sys/kernel/randomize_va_space"
sudo sh -c "echo $ORIG_GOV > /sys/devices/system/cpu/cpu$CPUID/cpufreq/scaling_governor"
sudo sh -c "echo $ORIG_TURBO > /sys/devices/system/cpu/intel_pstate/no_turbo"
sudo sh -c "echo $ORIG_SMT > /sys/devices/system/cpu/smt/control"
```
其中 make run 定義於 Makefile 中 ,Makefile 是用 [sysprog21/fibdrv](https://github.com/sysprog21/fibdrv) 去修改。
最後只須執行下面 shell ,即可完成效能測試。
```shell
這行只須執行一次
$ sudo chmod +x measure.sh
$ ./measure.sh
```
## zsmalloc xvmalloc 特性分析結論
### zsmalloc xvmalloc 效能比較
x : 分配的 block size 大小(範圍),假如 x 為 2000,分配的 block min 為 2000,block max 為 2000 + 64,單位為 bytes 。
y : 平均 malloc + free 的時間,單位為 us。
![](https://hackmd.io/_uploads/rkdW6AUuh.png)
對於 xvmalloc ,如同 TLSF 在 size 越大時因越容易觸發一些 if 的判斷,所以時間上升。到約為 PAGE_SIZE 一半時有了大幅的成長,這是因為 xvmalloc block 是以 page 為單位,舉例當你今天要了 PAGE_SIZE/2 的空間,加上 OVERHEAD 後,此 page 剩餘的空間已經不足 PAGE_SIZE/2 ,所以當又一次 allocate PAGE_SIZE/2 的大小時,只能動態的重新要一個 page 來儲存,所以時間大幅上升,而原本的 page 剩下的空間就浪費了。至於大於 PAGE_SIZE 的 allocate 就不會成功,所以時間趨近於 0。
對於 zsmalloc 也相同,一樣會隨著 size 上升而時間上升,在約為 3000 時時間會大幅上升,這是因為當 allocate 的 size 靠近 PAGE_SIZE 時,會大幅增加動態新增 zspage 的機率。至於大於 PAGE_SIZE 的 allocate 就不會成功,所以時間趨近於 0。
至於兩個 allocator 的比較,可以看出除了 2000~3000 的區間外, zsmalloc 都比 xvmalloc 耗時還要久,這是因為雖然 zsmalloc 可以減少碎片化,但是其實作時新增 zspage 的成本會比 xvmalloc 新增一個 page 的代價還高。
至於 2000~3000 的區間,為甚麼可以遠低於 xvmalloc ,這是因為 zsmalloc 使用 zspage 的方式實現,一個 zsapge 橫跨多個 page ,所以不像 xvmalloc 那樣當 size 接近 PAGE_SIZE/2 時所在 page 剩下的空間就無法使用, zspage 橫跨多個 page 所以當又分配 PAGE_SIZE/2 的空間時還是有可以容納的 object 可使用。
**雖然時間成本較高,但卻大幅減少了在 xvmalloc 中嚴重的空間浪費 (碎片化) 問題, 所以 zsmalloc 漸漸取代了 xvmalloc ,最新的 linux 版本中,已經沒有 xvmalloc 了。**
### zsmalloc xvmalloc 效能比較(大範圍)
測試 block 範圍大時,有什麼樣的現象。
x : 執行的 50 次相同的設定。
y : 平均 malloc + free 的時間,單位為 us。
左圖為使用 zsmalloc 動態獲取 free block 的方法。
右圖為使用調整後的 xvmalloc 方法。
不同線代表不同 size range。
<div style="display: flex;">
<img src="https://hackmd.io/_uploads/H1fwH_Qdh.png" width="50%" height="50%">
<img src="https://hackmd.io/_uploads/rkMvBOX_3.png" width="50%" height="50%">
</div>
結果與上一種測試相同,無論是哪種實作方式,平均 alloc 的 size 越大,所花的時間也會上升。
測試環境的 PAGE_SIZE 為 4096。
0 ~ 2048 bytes : 與上一種測試相同實作時新增 zspage 的成本會比 xvmalloc 新增一個 page 的代價還高。
0 ~ 4096 bytes : 可以發現 xvmlloc 所花費的時間遠遠低於 zsmalloc ,這是因為 xvmalloc 基於 TLSF 的優點,每次 allocate 完剩下的空間,會重新加入 free list 中,給其他次 allocate 使用,而 zsmalloc 則是對應大小的 object 用完後,就必須動態新增 zspage,**這是 xvmalloc 的優勢**。
2048 ~ 4096 bytes:可以發現兩種 allocator 的耗費時間相近,可以從上一種測試發現,雖然 3000 ~ 4096 zsmalloc 的成本較高,但在 2048 ~ 3000 時成本卻遠低於 xvmalloc ,平均下來兩者花費的平均時間就會相近,而雖然從此圖看不出來,xvmalloc 其實浪費了大量的空間,**這是 zsmalloc 的優勢**。
## 其他 allocator 比較
### with kmalloc
小於 PAGE_SIZE/2 則是 xvmalloc 表現最為優異。
在 $PAGE\_SIZE/2 ~ PAGE\_SIZE*2/3$ zmalloc 表現最為優異。
超過 $PAGE\_SIZE*2/3$ 則是 kmalloc 表現最為優異。
超過 PAGE_SIZE 後只有 kmalloc 表現能成功運行。
![](https://hackmd.io/_uploads/ryOL5CL_3.png)
### with vmalloc
要求超過 PAGE_SIZE 的空間在 kmalloc 以及 vmalloc 都有效,至於兩者差別還需要設計別的測試才可以知道。
![](https://hackmd.io/_uploads/BkqucCL_2.png)
參考資訊:
* [yvt/rlsf](https://github.com/yvt/rlsf)
* [xvMalloc](https://code.google.com/archive/p/compcache/wikis/xvMalloc.wiki)
* [AllocatorsComparison](https://code.google.com/archive/p/compcache/wikis/AllocatorsComparison.wiki)
* [War of allocators: TLSF in action](http://webkit.sed.hu/blog/20100324/war-allocators-tlsf-action)
* [mattconte/tlsf](https://github.com/mattconte/tlsf/blob/master/tlsf.c)
* [jserv/tlsf](https://github.com/jserv/tlsf-bsd)