# linux2021: AndybnACT ## 測驗 ϵ - 1 解釋上述程式碼運作原理: 在主程式中,我們可以看到有四個進入點,分別是將 memory pool 做初始化的 `mpool_init()`、配空間的 `mpool_alloc`、將空間歸還 memory pool 的 `mpool_repool`、以及釋放整個 memory pool 的 `mpool_free`。 程式運作的核心概念是根據配置大小對 pool 做分類,在每一個類別 (pool) 中用 array 來區隔每個可配置區、然後用 linked-list 將配置出來 array 中每個元素串成 free list 做配置及釋放(repool)的管理。 ### `struct mpool` ``` typedef struct { int cnt; /* actual pool count */ int pal; /* pool array length (2^x ceil of cnt) */ int min_pool, max_pool; /* minimum/maximum pool size */ void **ps; /* pools */ int *sizes; /* chunk size for each pool */ void *hs[1]; /* heads for pools' free lists */ } mpool; ``` 程式中,用上列結構來管理 memory pool。其中,`cnt` 是實際 memory pool 可配置分類區的數量。 `pal` 則是取 `cnt` 的 order 2 ceiling,如:若 `cnt` 為 5,則 `pal` 為大於等於 5 之最小 2 的冪次方值,8。這和 `mpool` 的擴充機制有關。 `pal` 會大於等於 `cnt` 的原因可以參考 `add_pool()`。當使用者需要配置某個大小的空間,而對應的 pool 僅剩下一個可配置區塊(free list 已經指向尾端)時,會透過 `mpool_new_pool()` 配置一塊完整的 pool,然後再由 `add_pool()` 將 free list 繼續串下去。而這些新產生的 pool 的管理,就都放在初始索引 `cnt` 之後的位置中、然後再將 `cnt++`。只有在 `cnt` 撞到頂(`cnt == pal`)的時候,透過 `realloc` 將 `pal` 配置為原先的兩倍(為 *dynamic array* 的做法,目的使得尾端的 insertion 和 `repool()/alloc()` 在找到 `i` 之後存取可以有 constant time。`i` 有 constant time 找法,但是 `alloc()` 沒有這樣做 )。把新產生的 pool 放進 `ps[]` 中還有的好處是:這樣之後在釋放整個 `mpool` 時,就不用再去找 `mpool_new_pool()` 產生的區塊位置,直接將 `ps[]` 的索引走完就即可。 `min_pool` 和 `max_pool` 用來標記 pools 中,能配置給使用者區塊大小的最大及最小值。用來做邊界判斷。 由 `ps`、 `sizes`、`cnt`、`pal` 所組成的 dynamic array 中,`ps` 代表的是分別是實際各 pool 的位置、`sizes` 則是各 pool 中對應的可配置大小,`cnt` 是當前 array 的尾端、`pal` 是當前 array 大小的邊界。 `hs` 在這個結構中,是 *flexible array member*,大小在初始配置時產生。代表的是各個可配置 pool 的 free list。此 array 的大小在初始(`init()`)配置後就是固定的,因為 free list 大小只和可配置區段的範圍有關。 ```c++ mpool *mp = malloc(sizeof(mpool) + (cnt - 1) * sizeof(void *)); ``` ### `mpool_init()` 用來初始化 memory pool、填上 dynamic array 各項初始數值。這裡還沒有實際將可配置區(pools)分配出來。可以看到 `pools`、`sizes`、`hs` 都被初始化成零。 ```c++ /* Initialize a memory pool for allocations between 2^min2 and 2^max2, * inclusive. Larger allocations will be directly allocated and freed * via mmap / munmap. * Returns NULL on error. */ mpool *mpool_init(int min2, int max2) { int cnt = max2 - min2 + 1; /* pool array count */ int palen = iceil2(cnt); assert(cnt > 0); mpool *mp = malloc(sizeof(mpool) + (cnt - 1) * sizeof(void *)); void *pools = malloc(palen * sizeof(void **)); int *sizes = malloc(palen * sizeof(int)); if (!mp || !pools || !sizes) return NULL; mp->cnt = cnt; mp->ps = pools; mp->pal = palen; mp->sizes = sizes; mp->min_pool = (1 << min2), mp->max_pool = (1 << max2); memset(sizes, 0, palen * sizeof(int)); memset(pools, 0, palen * sizeof(void *)); memset(mp->hs, 0, cnt * sizeof(void *)); return mp; } ``` ### `mpool_alloc()` 透過 `mpool_init()` 初始化 memory pool 之後,使用者就可以透過 `mpool_alloc` 向 memory pool 索取空間。 ```c++ /* Allocate memory out of the relevant memory pool. * If larger than max_pool, just mmap it. If pool is full, mmap a new one and * link it to the end of the current one. * Returns NULL on error. */ void *mpool_alloc(mpool *mp, int sz) { void **cur; int i, p; assert(mp); ``` 如果是超出 memory pool 所能提供的最大可配置大小,就直接用 `mmap()` 的方式和系統要空間。 ```c++ if (sz >= mp->max_pool) { cur = get_mmap(sz); /* just mmap it */ if (!cur) return NULL; return cur; } ``` 接著,根據 `sz` 找到對應 pool 的索引值。找尋索引的方式是在每次 iteration 中比較 `i` 對應到的 pool 可配置空間是否大於所需空間,然後再將可配置空間和 `i` 遞增。但其實這裡可以先將 sz 取 order 2 ceiling,若前後相等則再將 sz * 2。得到 sz 之後可用 find first set 的方式得到其為 order N 的配置,配合 `min_pool` 就可以得到 `i` ```c++ long szceil = 0; for (i = 0, p = mp->min_pool;; i++, p *= 2) { if (p > sz) { szceil = p; break; } } assert(szceil > 0); ``` 找到 `i` 之後,如果這是使用者第一次配置,那 free list 會是空的。這時透過 `mpool_new_pool()`,空間才會真正分配出來、串成 linked-list。然後才把 pool 加進 `mpool` 當中、並且也將 free list 指向 linked-list 的頭。 ```c++ cur = mp->hs[i]; /* get current head */ if (!cur) { /* lazily allocate and initialize pool */ void **pool = mpool_new_pool(szceil, PAGE_SIZE); if (!pool) return NULL; mp->ps[i] = mp->hs[i] = pool; mp->sizes[i] = szceil; cur = mp->hs[i]; } assert(cur); ``` 當中,linked-list 的串法由 `mpool_new_pool()` 完成。作法如下:先向系統請求一段連續區塊,然後將連續區段以可配置大小為單位切成數等分。最後在每一等份的頭,把它當作 `void *`,寫下下一等份的起始位置。 ![](https://i.imgur.com/65LB3Lb.jpg =200x) --- after 23:04 --- 在 pool 初次配置完成後,`*cur` 內的值應該會是 free list 中第二個元素。除非該 pool 的可配置大小比 `PAGE_SIZE` 還要大的時候,那整個 pool 會只有一個元素,此時 `(*cur)` 會是 `NULL`。其他符合此條件的情形有 free list 被拿到只剩最後一個 element 的時候。在這種情形下,會進入配置新 pool 的程式路徑。 配置的方法和第一次配置一樣,只是這個 pool 的相關資訊會透過 `add_pool()`,被加在 dynamic array 的尾端。因為 `mpool_alloc()` 一開始會檢查使用者需要的空間有沒有大於等於 `mpool->max_pool`,所以比 `max_pool` 還要大的配置不會誤入此區。 ```c++ if (!(*cur)) { /* if at end, attach to a new page */ void **np = mpool_new_pool(szceil, PAGE_SIZE); if (!np) return NULL; *cur = &np[0]; assert(*cur); if (add_pool(mp, np, szceil) < 0) return NULL; } assert(*cur > (void *) PAGE_SIZE); ``` 最後,更新 free list。只要將 free list 的頭指向 linked list 下一個元素的位置即完成更新。 ```c++ mp->hs[i] = *cur; /* set head to next head */ return cur; } ``` ### `mpool_repool()` 將配置出來的空間歸還給 pool。有了以 linked-list 為基礎的 free list,歸還空間的方式就是 list 的 insertion。因此我們只需要找到對應的 free list、把 free list 的頭替換成使用者歸還空間的位置、最後再將該位置起始 `sizeof(void *)` 的區段,寫成原本 free list 頭的位置,即完成 repool。 ```c++ /* Push an individual pointer P back on the freelist for the pool with size * SZ cells. if SZ is > the max pool size, just munmap it. * Return pointer P (SZ bytes in size) to the appropriate pool. */ void mpool_repool(mpool *mp, void *p, int sz) { int i = 0; if (sz > mp->max_pool) { if (munmap(p, sz) == -1) fprintf(stderr, "Failed to unmap %d bytes at %p\n", sz, p); return; } void **ip = (void **) p; ``` 因此,理想情況下 YYY 不能用一個表達式完成。因為要做的事包含了找到 `i`、以及改寫 `ip` 起始位置的值。可以用以下方式達成 ```c++ for(;(mp->min_pool << i) <= sz; i++) /* find i */; *ip = mp->hs[i]; ``` ```c++ YYY; assert(ip); mp->hs[i] = ip; } ``` ## 測驗 ϵ - 2 提出效能和功能的改進策略,並予以實作: 可以將所有「找 `i`」的片段做如下更改: ```diff= +#ifndef NEW for (i = 0, p = mp->min_pool;; i++, p *= 2) { if (p > sz) { szceil = p; break; } } assert(szceil > 0); +#else + if (sz < mp->min_pool) { + i = 0; + szceil = mp->min_pool; + } else { + szceil = iceil2(sz + 1); + long ffs_szceil = __builtin_ffs(szceil); + i = ffs_szceil - mp->min2 - 1; + } +#endif ``` 其中,`min2` 及 `max2` 用來記錄 `min_pool` 和 `max_pool` 二的冪次方 ```diff= typedef struct { int cnt; /* actual pool count */ int pal; /* pool array length (2^x ceil of cnt) */ int min_pool, max_pool; /* minimum/maximum pool size */ + int min2, max2; void **ps; /* pools */ int *sizes; /* chunk size for each pool */ void *hs[1]; /* heads for pools' free lists */ } mpool; ``` 測試環境如下: ``` CC = gcc-4.8 CFLAGS = -O2 -std=gnu11 cpuinfo: i9-7900X ``` 使用 `perf stat` 執行測試,可以看到 branch miss 在改動過後有顯著減少。推測是少了 loop 的重複判斷所致: ``` [andy@superstorm5 quiz]$ make gcc -std=gnu11 -O2 mpool.c [andy@superstorm5 quiz]$ perf stat ./a.out seed = 5df4ea Performance counter stats for './a.out': 483.227860 task-clock:u (msec) # 1.002 CPUs utilized 0 context-switches:u # 0.000 K/sec 0 cpu-migrations:u # 0.000 K/sec 90971 page-faults:u # 0.188 M/sec 558972494 cycles:u # 1.157 GHz 1189304467 instructions:u # 2.13 insn per cycle 279572903 branches:u # 578.553 M/sec 3989406 branch-misses:u # 1.43% of all branches 0.482204055 seconds time elapsed [andy@superstorm5 quiz]$ make new gcc -std=gnu11 -O2 -DNEW mpool.c [andy@superstorm5 quiz]$ perf stat ./a.out seed = 5df4ea Performance counter stats for './a.out': 475.900771 task-clock:u (msec) # 1.002 CPUs utilized 0 context-switches:u # 0.000 K/sec 0 cpu-migrations:u # 0.000 K/sec 90971 page-faults:u # 0.191 M/sec 540317858 cycles:u # 1.135 GHz 1231577718 instructions:u # 2.28 insn per cycle 269399773 branches:u # 566.084 M/sec 2489576 branch-misses:u # 0.92% of all branches 0.474930107 seconds time elapsed ``` ## 測驗 ζ - 1 解釋上述程式碼運作原理 ## 測驗 ζ - 2 以 epoll 系統呼叫改寫程式碼,並設計實驗來驗證 proxy 程式碼的效率