# Linux 核心專題: TLSF 實作 > 執行人: a1091150 #### 專題錄影與 GitHub 連結 - [專題解說錄影](https://youtu.be/O4YbqJFl1wk) - [GitHub: xvmalloc](https://github.com/a1091150/xvmalloc) :::success :question: 提問清單 * ? ::: ## 任務簡述 重做[第 6 週測驗題](https://hackmd.io/@sysprog/linux2023-quiz6)的測驗四,搭配第 5 週教材提及的 [tlsf-bsd](https://github.com/jserv/tlsf-bsd),探討何以 $O(1)$ 時間複雜度可落實於記憶體配置器 ## TODO: 研讀教材並紀錄認知及問題 至少要涵蓋: * [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) ### 研讀教材 #### 研讀論文 TLSF 在即時系統例如 RTOS (Real-Time Operating Systems) 下,每項操作或同種操作都需要在一定時間內回應。動態記憶體配置 (DSA; Dynamic storage allocation) 相關方法,在多數情況下有良好的回應時間,但是在 worst case 上可能超出時間。因此,在記憶體配置與釋放的操作上,TLSF 限定在 $\theta(1)$ 時間複雜度。 在論文中假設的應用場景:一定時間內回應、低程度的記憶體碎片、在有限或固定的記憶體配置上操作 ##### TLSF 核心機制 在論文中主要介紹: - 使用兩層去紀錄 free block - `mapping` 機制將要求的記憶體大小轉換到指定的層 ![](https://raw.githubusercontent.com/jserv/tlsf-bsd/master/assets/data-structure.png) TLSF 使用兩層位元,紀錄擁有的 free block - 第一層(First level):將 free block 以 2 的冪分類,使用對應位置的位元表示是否有 free block - 第二層(Second level):再將 free block 分成 4 個區間,分別用位元紀錄,透過串列去串接每個區間內的 free block 當程式要求記憶體 `size` 時,透過 `size` 計算分別對應第一層的位元與第二層位元,即可查詢是否有 free block。若在該位元沒有 free block,也可透過位元操作查詢鄰近的 free block。整個操作皆在 $\theta(1)$ 時間複雜度內。 在論文中,使用`mapping` 機制轉換 `size` 成為指定第一層與第二層位元,以 `size = 460` 為例: $$ size = 460_{10} = 0000 \space 000\overbrace{1}^{fl=8} \space \underbrace{1100}_{sl=12} \space 1100_2 $$ - 第一層的位元透過 find least set(fls) 計算取得 - 第二層的位元取第一層的位元之後 4 個位元 #### 研讀教材 Part 1: Background and Part 2: The floating point 參考 Part 1: Background 文中的`mapping_insert` 程式碼,該程式碼引用自 [GitHub - mattconte/tlsf](https://github.com/mattconte/tlsf/blob/master/tlsf.c#L513) ```c void mapping_insert(size_t size, int* fli, int* sli) { int fl, sl; if (size < 256) { fl = 0; sl = (int)size / 8; } else { fl = fls(size); sl = (int)(size >> (fl - 5)) ^ 0x20; fl -= 7; } *fli = fl; *sli = sl; } ``` - 計算第一層 `fl` 數值上,取 fls 數值 - 計算第二層 `sl` 數值上,與前述取得第一層位元的後 4 個位元相同,透過 `^ 0x20` 移除第一層的位元。 這裡有個特別的 `fl -= 7` 的程式碼,根據 Part 2: The floating point 文中敘述,可以將此 `mapping_insert` 當作把整數轉換程浮點數中 exponent 減去 bias 的過程,唯此轉換前後一定是正數。 #### 研讀 [tlsf-bsd](https://github.com/jserv/tlsf-bsd) tlsf-bsd 依照 TLSF 論文實作 `mapping` 機制,這裡來查看記憶體是如何被操作。 ##### 了解 `struct tlsf_block` 結構體 查看結構體 `struct tlsf_block` ```c /* Block status bits are stored in the least significant bits (LSB) of the * size field. */ #define BLOCK_BIT_FREE ((size_t) 1) #define BLOCK_BIT_PREV_FREE ((size_t) 2) 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; ``` - `size_t header;`: - 表示此 free block 的大小 - 透過記憶體對齊的特性,最低位元用來此 block 表示為 in-use block or free block,以 `BLOCK_BIT_FREE` 設定 - 透過記憶體對齊的特性,最低第二位元用來表示前一個 block 為 in-use block or free block,以 `BLOCK_BIT_PREV_FREE` 設定 - `struct tlsf_block *next_free, *prev_free;`:透過串列串接擁有相近大小的 free block - `struct tlsf_block *prev;`:除了串列串接相近大小的 free block 外,每個 free block 的最後一個元素被指派此 free block 的起始位址,讓下個 free block 可以透過此元素直接取得 free block 以合併。 當兩個 free block 合併前,參考下圖,`block2->prev` 指向 `block` 的最後一個元素,該元素有 `block1` 的記憶體位址。若把 `block1 size` 當作 `block1` 的一部分,可以認為這兩個 block 重疊。 ``` +--------+ <--- block1 | | +--------+ <--- block1->header | | +- +--------+ <--- block1->next_free | | | | | | size --+ | | | | | | +--------+ <--- block2->prev +- +--------+ <--- block2->header | | +--------+ ``` 當 free block 轉為 in-use block 時,`struct block_t` 經由 `*block_payload()` 計算 payload,回傳指定位址,以圖片區分 in-use block 與 free block 使用差異: ``` in-use block free block block ───► ┌───────┐ ┌───────┐ ◄─── block │ │ │ │ block->header ───► ├───────┤ ├───────┤ ◄─── block->header │ │ │ │ payload ───► ├───────┤ ├───────┤ ◄─── block->next_free │ │ │ │ │ │ ├───────┤ ◄─── block->prev_free │ space │ │ space │ │ │ │ │ └───────┘ └───────┘ ``` ##### 透過 `tlsf_malloc` 了解 `struct tlsf_block` 結構體位址計算 參考 Makefile 檔案,分別在 `test.c` 與 `bench.c` 以 `tlsf_resize()` 函式,向系統取得固定的記憶體空間。這裡解說 `gcc test.c tlsf.c` 編譯後的程式。 透過修改 `test.c` 的 `main()` 函式,觀察其行為 ```c // test.c int main(void) { PAGE = (size_t) sysconf(_SC_PAGESIZE); // 4096 MAX_PAGES = 20 * TLSF_MAX_SIZE / PAGE; tlsf_t t = TLSF_INIT; const size_t page_size = PAGE; void *p1 = tlsf_malloc(&t, (1 << 10) + 1); void *p2 = tlsf_malloc(&t, page_size - 1); tlsf_free(&t, p1); tlsf_free(&t, p2); } // tlsf.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); } ``` 要求記憶體時 `void *p1 = tlsf_malloc(&t, (1 << 10) + 1);`,會經由 - `tlsf_malloc(size)`:取得記憶體位址,將 `size` 透過 `adjust_size(size)` 進位到鄰近的 `ALIGN_SIZE = 1 << 3` 的倍數 - `block_find_free()`: 取得 free block 記憶體位址,但沒有找到 free block - `arena_grow()`:申請的記憶體空間,並作為 free block 加入到 tlsf level 中 - `tlsf_resize()`:透過 `mmap` 申請固定大小記憶體空間 - 再度透過 `block_find_free()` 取得 free block 觀察 `arena_grow` 中 `block` 的記憶體位址計算,來了解 `struct tlsf_block` 的每個成員作用: ```c INLINE tlsf_block_t *to_block(void *ptr) { tlsf_block_t *block = (tlsf_block_t *) ptr; return block; } #define BLOCK_OVERHEAD (sizeof(size_t)) static bool arena_grow(tlsf_t *t, size_t size) { size_t req_size = (t->size ? t->size + BLOCK_OVERHEAD : 2 * BLOCK_OVERHEAD) + size; void *addr = tlsf_resize(t, req_size); if (!addr) return false; 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->header |= size | BLOCK_BIT_FREE; block = block_merge_prev(t, block); block_insert(t, block); tlsf_block_t *sentinel = block_link_next(block); sentinel->header = BLOCK_BIT_PREV_FREE; t->size = req_size; check_sentinel(sentinel); return true; } ``` 第一次要求記憶體配置 `tlsf_malloc(&t, (1 << 10) + 1)`: - `req_size = 2 * BLOCK_OVERHEAD + size` - `block->header |= size | BLOCK_BIT_FREE;`,並加入到 tlsf level 中 - `block_link_next()` 連結下一個的 block,相當於指派 `sentinel->prev = block` - `sentinel` 表示記憶體中最後一個 block 在 `to_block()` 轉換成 free block 的位址計算,最後以圖表列出 `block` 與 `sentinel` 分別指向的位址 ``` +--------+ <--- block | | mmap of addr ---> +--------+ <--- block->header | 1088 | +- +--------+ <--- block->next_free | | | | | | size --+ | | | | | | +--------+ <--- sentinel->prev +- +--------+ <--- sentinel->header sizeof(size_t)| | | End of address ---> +- +--------+ ``` `block` 指向不可寫入的記憶位址,但是可以指派 `block->header`; `sentinel` 記錄最高位址,以重疊的區塊 `sentinel->prev` 紀錄 block 位址。 最終透過 `block_find_free()` 與 `block_use()` 回傳 payload 記憶體位址。 ## TODO: 解讀和改進程式碼 重做[第 6 週測驗題](https://hackmd.io/@sysprog/linux2023-quiz6)的測驗四,搭配上述來解釋何以 $O(1)$ 時間複雜度可落實於記憶體配置器,又如何改進實作。 此 heap allocator 與 tlsf-bsd 實作不同之處在於 - Level 設計差異: - TLSF 採取 first level 與 second level,first level 以位元位置跟 2 的冪區分、再以 second level 分成 4 個區間 - heap allocator 以一維陣列表示,對應成員 `headsbits` 陣列,陣列中的每個位元位置對應一種 base size。在此程式碼實作與命名中,每 15 個位元視為同個 level。 - Metadata 儲存方式差異: - TLSF 使用 `tlsf_block` 儲存該記憶體空間的 metadata,並以記憶體位址做減法以回推 `tlsf_block` - heap allocator 將 metadata 分離儲存,不能藉由記憶體做減法回推 metadata: - 利用 Linux 提供的 `posix_memalign` 使得記憶體對齊,將記憶體位址計算求出對應的 `headsbits` 與 `bitfield` - 成員 `bitfield` 用於紀錄鄰近記憶體空間的狀態,可視為二維陣列,若取其中的一維陣列的元素,可表示同個 level 下,該鄰近空間的使用情況。 ### 解讀程式碼 透過修改程式碼,並搭配 GDB 檢查變數數值、部分函式寫成 Python 程式碼,藉此觀察 `struct heap` 每個成員的作用。 ```c #define BASE_SIZE_MIN (0x00000010U) #define BASE_SIZE_MAX (0xF0000000U) #define BASE_SIZES_COUNT (105) #define MAIN_BASE_SIZE_COUNT (7U) #define HEADS_BITS_SIZE (((BASE_SIZES_COUNT + 31) >> 5)) struct heap { uint32_t headsbits[HEADS_BITS_SIZE]; // 4 uint32_t *bitfield[MAIN_BASE_SIZE_COUNT]; // 7 uint8_t *hdata; uint32_t hsize; uint32_t hdcnt; uint32_t bscnt; chunk_t *heads[0]; }; ``` - `uint32_t headsbits[HEADS_BITS_SIZE];`:某個位元對應的 base size 是否有 free block - `uint32_t *bitfield[MAIN_BASE_SIZE_COUNT]`:用於記錄該多個記憶體區塊的使用情況 - `uint8_t *hdata;`:傳入記憶體起始位址 - `uint32_t hdcnt;`:`bitfield` 陣列元素數量 - `chunk_t *heads[0];`:用於串接 free block。 free block 的記憶體空間會被用於串接每個 `chunk_t` 資料 #### `main()` 函式 將 `SIZE` 修改成較小的 `SIZE`,藉由引入與修改巨集數值 `#define BC 5` 來測試 `headsbits` 陣列數值。 每個位元代表不同的 base size ```c #define BC 5 int main(int argc, char *argv[]) { const uint32_t SIZE = (1 << 4) << BC; void *data = memalign(SIZE, SIZE); /* ... */ return 0; } ``` `void *data = memalign(SIZE, SIZE);` 使用 `posix_memalign()`,取得的記憶體位址與 `SIZE` 對齊。 可參考 `man posix_memalign` 說明 > `int posix_memalign(void **memptr, size_t alignment, size_t size);` > The address of the allocated memory will be a multiple of alignment, which **must be a power of two and a multiple of sizeof(void *)**. 因此 `SIZE` 具有以下特性: - 與 `void *` 對齊 - 只有一個位元被設定且為最高位 - 是 base size #### 解讀 `headsbits[HEADS_BITS_SIZE]` 成員 `H1->headbits` 是用來表示此 heap 以被設定的位元,此位元對應一種 base size,來表示擁有的 free block ##### 解讀方法 初始化 `headsbits` 使用到 `closest_base_size(SIZE)`, `base_size_to_index(used_size)` 與 `base_size_from_index(index)` 函式 - `closest_base_size(SIZE)` 函式將 `SIZE` 轉換成大於等於 `SIZE` 的 base size,由於 `SIZE` 對齊的特性,轉換前後數值相同 - `base_size_to_index(SIZE)` 函式將 `SIZE` 轉換為 base size 的索引值 - `base_size_from_index(index)` 函式為 `base_size_to_index(SIZE)` 的反函式 觀察函式 `base_size_from_index` 的輸出: | index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | |:--------------:| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | | 對應 base size | 16 | 32 | 48 | 64 | 80 | 96 | 112 | 128 | 144 | 160 | 176 | 192 | 208 | 224 | 240 | 256 | 512 | 768 | 1024 | 1280 | 1536 | 1792 | 2048 | 2304 | 2560 | 2816 | 3072 | 3328 | 3584 | 3840 | 4096 | 8192 | 從以上可以推得一個規律,每 15 位元視為同個 level,一共 7 個 level: - 第 00 ~ 14 位元:鄰近位元對應的 base size 之間相差一個 $16^1$ - 第 15 ~ 29 位元:鄰近位元對應的 base size 之間相差一個 $16^2$,第 15 位元為第 14 位元的 2 倍 - 第 30 ~ 44 位元:鄰近位元對應的 base size 之間相差一個 $16^3$,第 30 位元為第 29 位元的 2 倍 在 `heap_create()` 函式中,透過 `populate_heads()` 函式設定指定的位元。 ```c heap_t *heap_create(uint8_t *const address, uint32_t const size) { /* ... */ new_heap->headsbits[0] = 0; new_heap->headsbits[1] = 0; new_heap->headsbits[2] = 0; new_heap->headsbits[3] = 0x007FFFFFU; populate_heads(new_heap, address, size); /* ... */ } static void populate_heads(heap_t *const h, void const *const data, uint32_t const size) { /* ... */ uint32_t used_size = closest_base_size(size); uint32_t i = base_size_to_index(used_size); h->headsbits[i >> 5] |= 0x80000000U >> (i & 31); } ``` `h->headsbits[i >> 5] |= 0x80000000U >> (i & 31);` 將設定指定的位元,來表示 heap 中擁有的 free block。 #### 解讀 `bitfield[MAIN_BASE_SIZE_COUNT]` 成員 用來記錄每個 free block 使用的情形,利用 `chunk_status_t` ,以兩個位元表示對應的記憶體位置的區塊使用情況 ```c typedef enum { STATUS_FREE = 0x02U, STATUS_ALLOC = 0x00U, STATUS_ALLOC_HEAD = 0x01U, STATUS_SPLIT = 0x03U, } chunk_status_t; ``` 其中可以利用或修改以下函式,來查看對應的 `bitfield` 數值,或使用經由 `heap_alloc` 取得的記憶體位址,檢查該對應的 `chunk_status_t` 數值 ```c static chunk_status_t chunk_get_status(uint32_t const *const bf, uint32_t const idx); static uint32_t heap_get_address_status_priv(...) chunk_status_t heap_get_address_status(heap_t const *const h, void const *const a) ``` ##### 解讀方法 觀察 `heap_create()` 函式中藉由 `total_bitfield_count` 取得所有 `bitfield` 需要的記憶體空間,利用 `needed_bitfield_count` 計算分配給每個區域需要多少空間,`set_bf_ptr()` 初始化每個 `bitfield` 的數值。 ```c heap_t *heap_create(uint8_t *const address, uint32_t const size) { /* ... */ uint32_t const tot_bf_count = total_bitfield_count(size); void *const mem_bf = malloc(tot_bf_count * sizeof(uint32_t)); /* ... */ for (uint32_t i = 0, start = 0; i < MAIN_BASE_SIZE_COUNT; ++i) { uint32_t const nbc = needed_bitfield_count(size, i); set_bf_ptr(i, nbc, new_heap, start, mem_bf, size); start += nbc; } /* ... */ } ``` 仿照 `needed_bitfield_count()` 函式撰寫成 Python 程式碼觀察其數量 ```python def needed_bitfield_count(size: int, index: int): count = size >> ((index + 1) << 2) return (count + 15) // 16 def show_need_bitfield_each_alloc(i: int): size = 1 << i xx = [needed_bitfield_count(size, x) for x in range(7)] return xx ``` 輸入參數`SIZE = 1 << i` 來表示原本 `SIZE = 1 << 4 << BC` 的輸入,輸出指標陣列中 `bitfield[MAIN_BASE_SIZE_COUNT]` 每個陣列的元素數量。 ```python In [3]: show_need_bitfield_each_alloc(4) # SIZE = 1 << 4 Out[3]: [1, 0, 0, 0, 0, 0, 0] In [4]: show_need_bitfield_each_alloc(5) # SIZE = 1 << 5 Out[4]: [1, 0, 0, 0, 0, 0, 0] In [5]: show_need_bitfield_each_alloc(6) # SIZE = 1 << 6 Out[5]: [1, 0, 0, 0, 0, 0, 0] In [6]: show_need_bitfield_each_alloc(7) # SIZE = 1 << 7 Out[6]: [1, 0, 0, 0, 0, 0, 0] In [7]: show_need_bitfield_each_alloc(8) Out[7]: [1, 1, 0, 0, 0, 0, 0] In [8]: show_need_bitfield_each_alloc(9) Out[8]: [2, 1, 0, 0, 0, 0, 0] In [10]: show_need_bitfield_each_alloc(10) Out[10]: [4, 1, 0, 0, 0, 0, 0] In [11]: show_need_bitfield_each_alloc(11) Out[11]: [8, 1, 0, 0, 0, 0, 0] In [12]: show_need_bitfield_each_alloc(12) Out[12]: [16, 1, 1, 0, 0, 0, 0] In [13]: show_need_bitfield_each_alloc(13) Out[13]: [32, 2, 1, 0, 0, 0, 0] ``` `**bitfield` 型態為 `uint32_t`,以兩個位元表示 `chunk_status_t` 狀態,因此一個 `**bitfield` 元素可以表示 16 個記憶體區塊的狀態。 以 `SIZE = 1024 = 1 << 10 = 1 << 4 << 6` 為例,一次最小配置的記憶體是 16 位元組: - 若全部皆以 `heap_alloc(H1, 16)` 取得記憶體位址,則需要 $2^{6}$ 個 `chunk_status_t` 去記錄狀態,可用 $2^6 / 16 = 2^2 = 4$ 個 `uint32_t`,符合 Python 程式輸出 - 若全部皆以 `heap_alloc(H1, 256)` 取得記憶體位址,則需要 $2^{2}$ 個 `chunk_status_t` 去記錄狀態,可用 $\lceil 2^2 / 16 \rceil = 1 = 1$ 個 `uint32_t`,符合 Python 程式輸出 ###### 觀察 `bitfield` 數值狀態 透過修改 `chunk_get_status()` 函式中,新增 `printf("%u %x\n", idx >> 4, bf[idx >> 4]);` 觀察 `bitfield` 數值變化。 ```c static chunk_status_t chunk_get_status(uint32_t const *const bf, uint32_t const idx) { uint32_t const sub = ~idx & 15u; printf("%u %x\n", idx >> 4, bf[idx >> 4]); return (bf[idx >> 4] >> (sub << 1)) & 0x03U; } /* ... */ #define BC 4 int main(int argc, char *argv[]) { const uint32_t SIZE = (1 << 4) << BC; /* ... */ void *foo[(1 << BC)]; for (int i = 0; i < (1 << BC); i++) { foo[i] = heap_alloc(H1, 16); printf("%02d ", i); chunk_status_t cs = heap_get_address_status(H1, foo[i]); } return 0; } ``` 每行 `heap_alloc` 函式依序輸出 `foo` 索引值、`bitfield` 索引值、該 `bitfield` 數值, 單一行`heap_free` 函式輸出`bitfield` 索引值、該 `bitfield` 數值 ``` 00 0 aaaaaaa9 01 0 aaaaaaa5 02 0 aaaaaa95 03 0 aaaaaa55 04 0 aaaaa955 05 0 aaaaa555 06 0 aaaa9555 07 0 aaaa5555 08 0 aaa95555 09 0 aaa55555 10 0 aa955555 11 0 aa555555 12 0 a9555555 13 0 a5555555 14 0 95555555 15 0 55555555 ``` 觀察 `heap_alloc()` 函式輸出情況,以第一行 `00 0 aaaaaaa9` 為例,最低十六進制的數值為 $9_{10} = 1001_2$,取最低兩個位元,得到 `chunk_status_t` 狀態為 `STATUS_ALLOC_HEAD = 0x01U` #### 解讀 `chunk_t *heads[0];` 成員 `*heads[0];` 可參考 [GNU Zero Length](https://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html) 說明,GNU Extension 支援 在前述 `headsbits` 的位元用來表示是否有 free block, `chunk_t *heads[0];` 則用來串接每個 free block 的記憶體位址。 ```c typedef struct chunk { struct chunk *prev, *next; } chunk_t; ``` ##### 解讀方法 觀察 `heap_create()` 初始化的方式與使用 `heap_alloc` 時候其使用的 `chunk_t` 的方式 ```c heap_t *heap_create(uint8_t *const address, uint32_t const size) { /* ... */ uint32_t const cs = CLZ(size) & 0x1CU; uint32_t const hd_cnt = ((24 - cs) >> 2) * 15 + ((size >> (28 - cs)) & 0x0FU); heap_t *new_heap = malloc(sizeof(*new_heap) + (hd_cnt * sizeof(chunk_t *))); populate_heads(new_heap, address, size); } static void populate_heads(heap_t *const h, void const *const data, uint32_t const size) { uint32_t used_size = closest_base_size(size); uint32_t i = base_size_to_index(used_size); h->heads[i] = (chunk_t *) data; h->heads[i]->prev = NULL; h->heads[i]->next = NULL; /* ... */ } ``` 在 `heap_create()` 中,`hd_cnt` 的實際數值剛好是 `base_size_to_index(size) + 1`。例如 `SIZE = 1 << 28`, `hd_cnt = 91` 且 `base_size_to_index(SIZE) = 90`,在 `populate_heads` 函式中,剛好是指定 `h->heads[90]` 陣列中擁有 free block 經過多個 `heap_alloc()` 與 `heap_free()` 交錯執行後,該 free block 的記憶體空間會被轉換成並用於 `chunk_t` #### 解讀 `heap_alloc()` 函式 經過前述 `heap_t` 結構體成員的每個作用,多少可以推得 O(1) 如何的運作模式。 來解讀 `heap_alloc(heap_t *const h, uint32_t const sz)` 函式實作,主要有以下步驟: - 經由 `next_available_head_index()` 函式取得,尋找符合 `needed_sz` 的 free block。 - 在符合的 free block 中,取得符合 `needed_sz` 的 free block,並將剩餘 free block 切割並加入至對應大小的 `h->heads` 中 以 `SIZE = 1 << 16 = 65536` 為例,查看其中一種程式碼行為: ```c=1 #define BC 12 int main(int argc, char *argv[]) { const uint32_t SIZE = (1 << 4) << BC; void *data = memalign(SIZE, SIZE); heap_t *H1 = heap_create(data, SIZE); void *foo = heap_alloc(H1, 16); /* ... */ return 0; } void *heap_alloc(heap_t *const h, uint32_t const sz) { /* ... */ uint32_t const index = next_available_head_index(h, needed_sz); chunk_t *c = h->heads[index]; uint32_t const extra_sz = found_sz - needed_sz; /* the combined number of iterations for both 'for' loops in this * function is 7 max, hence the 0(1) complexity */ for (;; --bs_level, shift -= 4) { uint32_t const lvl_remain_sz = (extra_sz >> shift) & 0x0FU; if (0 != lvl_remain_sz) { uint32_t const head = (bs_level << 4) - bs_level + lvl_remain_sz - 1; chunk_t *const hd = h->heads[head]; update_next(h, c, hd); update_prev(h, c, NULL); update_head(h, head, c); if (hd) { update_prev(h, hd, c); } c = (chunk_t *) ((uint8_t *) c + (lvl_remain_sz << shift)); } lvl_needed_sz = needed_sz >> shift; if (0 != lvl_needed_sz) break; uint32_t const split = ((uint32_t) c - base) >> shift; bf_set_split(h->bitfield[bs_level], split); } /* ... */ } ``` 當 `heap_alloc(H1, 16)` 時 - `chunk_t *c` 變數為回傳的記憶體位址 - `next_available_head_index` 取得 `index = 45` 對應原本 `SIZE = 65536` 的 free block - `65536 - 16 = 65520`,可以預期將 `65520` 分割成不同的 base size:`65520 = 61440 + 3840 + 240` - `61440` 對應 base size index 為 44 - `3840` 對應 base size index 為 29 - `240` 對應 base size index 為 14 - 在第 20 行程式碼,先將剩餘的 free block 將入到 base size 對應的 `h->heads[head]` 之中,再計算 `c` 的記憶體位址 根據註釋 ``` /* the combined number of iterations for both 'for' loops in this * function is 7 max, hence the 0(1) complexity */ ``` 可以設想最多的執行次數的 worst case,例如 `SIZE = 1 << 31` 並且 `heap_alloc(H1, 16)` 取最小單位的空間,最終在所有 7 個 level 中,都放置一個 free block ```c #define BC 27 int main(int argc, char *argv[]) { const uint32_t SIZE = (1 << 4) << BC; void *data = memalign(SIZE, SIZE); heap_t *H1 = heap_create(data, SIZE); void *foo = heap_alloc(H1, 16); /* ... */ return 0; } ``` ## TODO: 研究 Linux 核心的 xvmalloc 和 zsmalloc 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) 的測試和效能評比框架,分析其效益。 參考資訊: * [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) #### xvmalloc: 探討核心開發者的考量的因素 在 git log 中未有標注原因,在 LWN.net 搜尋 xvmalloc,有以下描述: 節錄自 [LWN.net: In-kernel memory compression](https://lwn.net/Articles/545244/#:~:text=To%20improve%20density,through%20Linux%203.8.), [The zsmalloc allocator](https://lwn.net/Articles/477067/#:~:text=The%20%22zsmalloc%22%20allocator,within%20a%20page.) > To improve density, the xvmalloc allocator was written, based on the two-level sequential fit (TLSF) algorithm. > Xvmalloc provided high density for some workloads and was the initial default allocator for zram and for the frontswap-driven portion of the initial zcache. > It was found, however, **that the high density led to poor fragmentation characteristics, especially for zsize distributions that skewed fat.** > Xvmalloc has since been discarded (though remnants of it survive through Linux 3.8.) > > The "zsmalloc" allocator, proposed by Seth Jennings, is aimed at a specific use case. > The slab allocators work by packing multiple objects into each page of memory; that works well when the objects are small, but can be problematic as they get larger. > In the worst case, if a kernel subsystem routinely needs allocations that are **just larger than PAGE_SIZE/2, only one object will fit within a page.** > > [staging: zsmalloc: memory allocator for compressed pages](https://lwn.net/Articles/474880/) > This patchset introduces a new memory allocation library named > zsmalloc. zsmalloc was designed to fulfill the needs > of users where: > 1) Memory is constrained, preventing contiguous page allocations > larger than order 0(single page) and > 2) Allocations are all/commonly greater than half a page. 在 xvmalloc 實作中,如果絕大多數的要求記憶體空間,接近並大於 `PAGE_SIZE / 2`,則該 page 只會有一個物件,形成嚴重的記憶體浪費。 zsmalloc 主要解決此使用情境。 :::warning :warning: 應闡述什麼場景特別重視上述的記憶體運用方式。 :notes: jserv ::: #### 移植 xvmalloc > [Github: xvmalloc](https://github.com/a1091150/xvmalloc) xvmalloc 與前面 TLSF-bsd 實作上不同: - 記憶體配置 `xv_malloc(... size)` 只能要求小於一個 page size(Linux 上 page size = 4096) - `size` 會進位成 8 的倍數,包含本身 4 位元組的 block 開銷,最大配置大小為 4088 位元組(4096 - 8) 在移植 xvmalloc 的時候,考量以上特性,把 tlsf-bsd 中的測試流程修改成 xvmalloc 的應用情境 ##### 解說移植後程式碼 目前的程式碼可以在 macOS 與 Ubuntu Linux 22.04 上運作。 在移植的方式上,盡量不更動 xvmalloc 程式碼,透過撰寫缺少的函式、結構體。 - `xv_malloc.c`, `xv_malloc.h` 與 `xv_malloc_int.h`:移植後的程式碼主體 - `types.h`:移植必要的巨集 - `test_page.h`:移植 linux module header 的函式的標頭檔 - `test.c`:主要撰寫 `main()` 函式與 benchmark 在原本的 xvmalloc 中,每次空間不足時,透過 `alloc_page()` 向系統申請一個 page,使用 `xv_malloc()` 會取得 page 結構體與 offset,以此取得記憶體位址。 來查看真如 LWN 敘述的記憶體浪費的情形。在 `test.c` 透過配置兩組指定記憶體大小,大於 `page size / 2` 與 `4`,並限制總體 page 數量,即可發現記憶體配置失敗,符合 LWN 敘述的記憶體浪費: ```c int main(int argc, char **argv) { MAX_PAGES = 1; // Only one page can be required /* ... */ test_alloc(); /* ... */ } void test_alloc() { struct xv_pool *pool = xv_create_pool(); struct page *page; u32 offset; const u32 size = XV_MAX_ALLOC_SIZE / 2 + 1; int x1 = xv_malloc(pool, size, &page, &offset); int x2 = xv_malloc(pool, 4, &page2, &offset2); // Fail to acquire memory /* ... */ } ``` ### benchmark 使用測量環境 ``` 5.19.0-43-generic Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Address sizes: 39 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 4 On-line CPU(s) list: 0-3 Vendor ID: GenuineIntel Model name: Intel(R) Core(TM) i5-4250U CPU @ 1.30GHz gcc (Ubuntu 11.3.0-1ubuntu1~22.04.1) 11.3.0 ``` 依照 tlsf-bsd 中的測試方式: - 若已取得記憶體,低機率 `realloc()` 或高機率 `tlsf_free()` - 若無記憶體空間,要求記憶體 `tlsf_malloc()` ```c static void run_alloc_benchmark() { while (loops--) { size_t next_idx = (size_t) rand() % num_blks; size_t blk_size = get_random_block_size(blk_min, blk_max); if (blk_array[next_idx]) { if (rand() % 10 == 0) { blk_array[next_idx] = tlsf_realloc(&t, blk_array[next_idx], blk_size); } else { tlsf_free(&t, blk_array[next_idx]); blk_array[next_idx] = tlsf_malloc(&t, blk_size); } } else { blk_array[next_idx] = tlsf_malloc(&t, blk_size); } if (clear) memset(blk_array[next_idx], 0, blk_size); } /* Free up all allocated blocks. */ for (size_t i = 0; i < num_blks; i++) { if (blk_array[i]) tlsf_free(&t, blk_array[i]); } } ``` 在 xvmalloc 中沒有 `realloc` 函式,直接移除 `realloc` 函式,限制記憶體配置範圍 32 ~ 4088。 ```c static void run_alloc_benchmark() { struct xv_pool *pool = xv_create_pool(); assert(pool); while (loops--) { size_t next_idx = (size_t) rand() % num_blks; size_t blk_size = get_random_block_size(blk_min, blk_max); if (blk_array[next_idx]) { struct page *page = blk_array[next_idx]; u32 offset = blk_offset_array[next_idx]; xv_free(pool, page, offset); blk_array[next_idx] = NULL; blk_offset_array[next_idx] = 0; } else { struct page *page; u32 offset; int r = xv_malloc(pool, blk_size, &page, &offset); if (!r) { blk_array[next_idx] = page; blk_offset_array[next_idx] = offset; } } if (clear && blk_array[next_idx]) { char *addr = blk_array[next_idx]; u32 offset = blk_offset_array[next_idx]; memset(addr + offset, 0, blk_size); } } /* ... */ } ``` 獲得輸出: ```shell $ gcc xvmalloc.c test.c $ ./a.out blk_min=512 to blk_max=512 512:512:10000000:0:87900160:0.703468: took 0.703468 s for 10000000 malloc/free benchmark loops of 512-512 bytes. ~0.070 us per loop ./a.out -s 32:4088 blk_min=32 to blk_max=4088 32:4088:10000000:0:96399360:1.237614: took 1.237614 s for 10000000 malloc/free benchmark loops of 32-4088 bytes. ~0.124 us per loop ./a.out -s 32:4088 -c blk_min=32 to blk_max=4088 32:4088:10000000:1:96399360:1.592345: took 1.592345 s for 10000000 malloc/free benchmark loops of 32-4088 bytes. ~0.159 us per loop ``` 與 tlsf-bsd 對比 ```shell $ ./build/bench blk_min=512 to blk_max=512 512:512:10000000:0:6025216:0.480467: took 0.480467 s for 10000000 malloc/free benchmark loops of 512-512 bytes. ~0.048 us per loop $ ./build/bench -s 32 blk_min=32 to blk_max=512 32:512:10000000:0:3923968:0.611649: took 0.611649 s for 10000000 malloc/free benchmark loops of 32-512 bytes. ~0.061 us per loop $ ./build/bench -s 10:12345 blk_min=10 to blk_max=12345 10:12345:10000000:0:68145152:1.400485: took 1.400485 s for 10000000 malloc/free benchmark loops of 10-12345 bytes. ~0.140 us per loop ``` 用 `perf stat --repeat 100 ./a.out -s 32:4088` 去測量 ``` Performance counter stats for './a.out -s 32:4088' (100 runs): 2,666.90 msec task-clock # 1.000 CPUs utilized ( +- 0.18% ) 22 context-switches # 8.333 /sec ( +- 5.54% ) 2 cpu-migrations # 0.758 /sec ( +- 25.99% ) 23,383 page-faults # 8.857 K/sec ( +- 0.00% ) 6,884,795,731 cycles # 2.608 GHz ( +- 0.10% ) 7,809,646,003 instructions # 1.15 insn per cycle ( +- 0.00% ) 1,003,886,833 branches # 380.259 M/sec ( +- 0.00% ) 24,409,076 branch-misses # 2.43% of all branches ( +- 0.02% ) 2.66722 +- 0.00591 seconds time elapsed ( +- 0.22% ) ``` 用 `perf stat --repeat 100 ./a.out -s 32:4088 -c` 測量,`-c` 參數會將取得的記憶體空間指派 0。 ``` Performance counter stats for './a.out -s 32:4088 -c' (100 runs): 3,740.16 msec task-clock # 1.014 CPUs utilized ( +- 0.18% ) 26 context-switches # 7.100 /sec ( +- 4.53% ) 0 cpu-migrations # 0.000 /sec 23,384 page-faults # 6.385 K/sec ( +- 0.00% ) 9,560,078,934 cycles # 2.610 GHz ( +- 0.15% ) 8,235,466,240 instructions # 0.87 insn per cycle ( +- 0.00% ) 1,068,148,108 branches # 291.669 M/sec ( +- 0.00% ) 28,288,702 branch-misses # 2.65% of all branches ( +- 0.09% ) 3.68915 +- 0.00996 seconds time elapsed ( +- 0.27% ) ``` 使用 ```shell $ perf record -g --call-graph dwarf ./a.out -s 32:4088 -c $ perf report --stdio -g graph,0.5,caller > foo.txt ``` 得到以下結果 :::spoiler Perf Graph ``` main | |--93.41%--bench_main | | | --93.33%--run_alloc_benchmark | | | |--29.72%--xv_free | | | | | |--5.65%--test_flag | | | | | |--4.58%--insert_block | | | | | | | --1.27%--__set_bit | | | | | |--2.52%--remove_block | | | | | | | --0.66%--get_ptr_atomic | | | | | |--1.15%--set_flag | | | | | |--0.93%--get_ptr_atomic | | | | | --0.75%--BLOCK_NEXT | | | |--28.92%--xv_malloc | | | | | |--5.59%--remove_block_head | | | | | | | --0.70%--__clear_bit | | | | | |--5.01%--find_block | | | | | | | |--0.75%--get_index | | | | | | | --0.56%--test_bit | | | | | |--3.54%--set_flag | | | | | |--3.52%--grow_pool | | | | | | | |--1.77%--alloc_page | | | | | | | | | --1.33%--find_page_unused | | | | | | | --0.55%--set_flag | | | | | |--2.50%--insert_block | | | | | | | --0.68%--__set_bit | | | | | |--0.84%--get_ptr_atomic | | | | | |--0.80%--clear_flag | | | | | |--0.69%--set_blockprev | | | | | --0.51%--put_ptr_atomic | | | |--11.96%--rand | | | | | --11.87%--__random | | | | | --1.26%--__random_r | | | |--7.33%--__memset_avx2_unaligned_erms | | | |--6.78%--get_random_block_size | | | | | --4.55%--rand | | __random | | | | | --2.68%--__random_r | | | --1.59%--0x55b65b21a214 ``` ::: ## 附錄 RLSF - 可參考官方文件 [Rust Doc RLSF](https://docs.rs/rlsf/latest/rlsf/#structs) - [GitHub RLSF](https://github.com/yvt/rlsf) :::warning 我剛開始學習 Rust,因此可能部分環節有誤。 ::: 在有之前 TLSF 的認識之後,來了解如何使用 Rust 去實作,參考 RLSF 範例。 ```rust fn main() { let mut pool = [MaybeUninit::uninit(); 65536]; let mut tlsf: Tlsf<'_, u16, u16, 12, 16> = Tlsf::new(); tlsf.insert_free_block(&mut pool); } ``` 先來了解 Rust 語法跟其作用: > MaybeUninit,可參考 [Rust Doc: MaybeUninit](https://doc.rust-lang.org/stable/core/mem/union.MaybeUninit.html) > A wrapper type to construct uninitialized instances of T > 建立一變數,Rust 會初始化該數值,例如 `let x: i32` 會自動初始化為 0。 > 透過 `MaybeUninit` 取得未初始化的記憶體空間。 > > [type; length] 語法糖:建立一組陣列 [Rust Vec](https://doc.rust-lang.org/stable/std/vec/index.html) > A contiguous growable array type with heap-allocated contents, written Vec > 在記憶體空間上為連續。 `pool` 變數取得的連續記憶體空間,相當於 C 語言的 `void *pool = malloc(65536)` ,作為 block 加入至 tlsf allocator 中。 ```rust pub struct Tlsf<'pool, FLBitmap, SLBitmap, const FLLEN: usize, const SLLEN: usize> { fl_bitmap: FLBitmap, /// `sl_bitmap[fl].get_bit(sl)` is set iff `first_free[fl][sl].is_some()` sl_bitmap: [SLBitmap; FLLEN], first_free: [[Option<NonNull<FreeBlockHdr>>; SLLEN]; FLLEN], _phantom: PhantomData<&'pool ()>, } ``` 依據 [Rust Doc: Generic Type](https://doc.rust-lang.org/book/ch10-01-syntax.html) 特性,可在編譯時期決定該型態: - `FLBitmap` 成員:First level 的位元對應的 Rust 整數型態 - `SLBitmap` 成員:Second level 的位元對應的 Rust 整數型態 - `FLLEN` 成員:First level 用到的位元數量 - `SLLEN` 成員:Second level 用到的位元數量 - `_phantom` 成員:根據 [Rust Doc: PhantomData](https://doc.rust-lang.org/std/marker/struct.PhantomData.html): Zero-sized type used to mark things that “act like” they own that data type. Rust 強調所有權(Ownership),這裡假裝擁有資料的所有權。 `'pool` 語法可參考 [Rust by Example: Explicit annotation](https://doc.rust-lang.org/rust-by-example/scope/lifetime/explicit.html),表示在 TLSF 結構體執行時間,`pool` 的生命週期夠長(活得夠久) 根據註釋,此實作版本的 RLSF 最低配置 16 位元組 ```svgbob First level "FLLEN = 8" ╭─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────╮ "fl_bitmap: FLBitmap ="│ 0 │ 0 │ 0 │ 1 │ 0 │ 0 │ 0 │ 0 │ ├─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ "min size"│ 2¹¹ │ 2¹⁰ │ 2⁹ │ 2⁸ │ 2⁷ │ 2⁶ │ 2⁵ │ 2⁴ │ ╰─────┴─────┴─────┴──┬──┴─────┴─────┴─────┴─────╯ ``` 將 `pool` 當作 free block 加入到 `TLSF` 中 ```rust pub fn insert_free_block(&mut self, block: &'pool mut [MaybeUninit<u8>]) -> impl Send + Sync { // Safety: `block` is a mutable reference, which guarantees the absence // of aliasing references. Being `'pool` means it will outlive `self`. unsafe { self.insert_free_block_ptr(NonNull::new(block as *mut [_] as _).unwrap()) }; } ``` 參考 [The Rust Programming Guide: unsafe](https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html),使用 `unsafe` 去操作與 C 語言實作中相同的記憶體位址計算: > To switch to `unsafe` Rust, use the unsafe keyword and then start a new block that holds the unsafe code. > You can take five actions in unsafe Rust that you can’t in safe Rust, which we call unsafe superpowers. > Those superpowers include the ability to: > - Dereference a raw pointer > - Call an unsafe function or method > - Access or modify a mutable static variable > - Implement an unsafe trait > - Access fields of unions 透過 [Rust raw pointer](https://doc.rust-lang.org/reference/types/pointer.html#mutable-references-mut),將該 block 的記憶體位址取出。 :::info 透過文件查詢,可以使用內建的函式 [`core::ptr::addr_of`](https://doc.rust-lang.org/core/ptr/macro.addr_of.html) 取得記憶體位址。 在 Rust Playground 實驗下,可獲得一樣的輸出 ```rust fn main() { let a = 42; let memory_location = &a as *const i32 as usize; let mf = core::ptr::addr_of!(a) as usize; println!("{}", memory_location); // 140723548430676 println!("{}", mf); // // 140723548430676 } ``` ::: `NonNull` 可當作是支援 raw pointer 的結構體,參考 [Rust Doc: NonNull](https://doc.rust-lang.org/stable/core/ptr/struct.NonNull.html) > This is often the correct thing to use when building data structures using raw pointers, but is ultimately more dangerous to use because of its additional properties. > If you’re not sure if you should use NonNull\<T\>, just use *mut T!