# 2020q1 Homework3 (quiz4) contributed by < `eecheng87` > ###### tags: `linux2020` > [第 4 週測驗題](https://hackmd.io/@sysprog/linux2020-quiz4) ## 題目 `1` ### 解釋程式運作原理 首先,觀察如何呼叫 `bitcpy`,從參數著手: * src : 代表欲被複製的起始位址 * read : 為 src 的偏移量 ( 真正開始複製的位址 ) * dest : 代表欲被貼上的起始位址 * write : 為 dest 的偏移量 ( 真正開始貼上的位址 ) * count : 代表要複製的位元數 舉例如何呼叫 `bitcpy` : ```cpp memset(&input[0], 0xFF, sizeof(input)); memset(&output[0], 0x00, sizeof(output)); bitcpy(&output[0], 1, &input[0], 1, 3); dump_binary(&output[0], 1); ``` ```shell 01100000 ``` 解釋 : 要複製 `input[0]` 的第 1 個位元往後數 3 個 ( $111_b$ ) 到 `output[0]` 的第 1 個位元 ( 左邊表示資料最高位 )。 再來,我們開始看 `bitcpy` 是如何實作的 ```cpp .. data = *source++; bitsize = (_count > 8) ? 8 : _count; if (read_lhs > 0) { data <<= read_lhs; if (bitsize > read_rhs) data |= (*source >> read_rhs); } .. ``` 進入 while 迴圈作的第一件事,把 8 個 bit 的資料複製到 `data`。但是又分成兩種情況,第一種是最簡單的,若我們的 `_read` 是 0 ( 不位移 ),那我們可以直接把 `source` 的 8 個 bit 給 data。另外一種情況是,並非從 `source` 的第一個位元開始複製。 用實際例子說明狀況二: 假設 `_read = 3` ( 從第三個位元開始複製 ),則我們會得到 `read_lhs = 3; read_rhs = 5`。目前我們得到的 `data` 是從 `source` 第 0 位元開始的資料,所以我們要把 `data` 往左移 3 格 ( 即 `read_lhs` )。所以目前 `data` 由左數過來 5 個 bits 都是我們要的,但是因為上一個步驟往左移,最右邊 3 個 bit 變成 $000_b$,這不是我們要的。我們要的是,下一個 `source` 的左邊三個 bit 。因為我們在上面已經移動 `source` 了 ( `source++` ),所以我們不需要再動,直接取左三 bit 然後再透過 `|` 寫到 `data` 右邊三個 bit。這樣我們就得到我們想要的前 8 個 bit 了 。 ```cpp .. if (bitsize < 8) data &= read_mask[bitsize]; .. ``` 接著如果 目前還需要再讀的 bit 小於 8 的話,我們透過 mask 把多餘的部分變成 0。 ```cpp original = *dest; if (write_lhs > 0) { .. } else { if (bitsize < 8) data = data | (original & write_mask[bitsize]); *dest++ = data; } ``` 再來是處理寫到 `dest` 的部分,分成兩種情境,寫的位址是否有位移值,若沒有則是比較單純的狀況 ( `else` ) ,我們先來看看若不用位移的話怎麼處理。 目前我們的 `data` 是經過前面**讀的位移**處理後拿到的資料,這裡面全部都是我們要的,所以可以直接給 `dest`。但是如果我們剩下需要複製的位元小於 8 的話,我們需要透過 `mask` 把 `data` 右邊不要的位元清掉。 ```cpp mask = read_mask[write_lhs]; if (bitsize > write_rhs) { .. } else { .. } ``` 再來,我們來觀察若寫需位移要怎麼處理。這裡的 `mask` 是用來保留左邊的 bit 和清除右邊的 bit ( 因為若有位移,我們不能汙染到左邊的 bit )。 這次我們同樣用舉例的方式來了解程式原理 : 假設`_write = 3` ( 從第三個位元開始貼上 ),則我們會得到 `write_lhs = 3; write_rhs = 5`。若該次需要寫的位元超過 `write_rhs`,代表接下來要貼上的 8 位元會跨一個單位 ( 8 位元 )。所以我們要做以下處理 : ```cpp *dest++ = (original & mask) | (data >> write_lhs); original = *dest & write_mask[bitsize - write_rhs]; *dest = original | (data << write_rhs); ``` 分別是 : * 把前五個 bit 寫到 `dest` ( 注意不能污染前三個 bit,然後把 `dest` 往下一格移動 ) * 把下一格 `dest` 的右邊 5 個 bit 保留好存到 `original` ( 左邊 3 個清成 0 ) * 把 `original` 往左移 5 bit,所以目前的前三 bit 就是上次 `data` 還沒複製到 `dest` 的資料。透過 `|` 把第二步驟留下的左三 bit 填滿 接著第二種情況就比較單純了,不用考慮跨單位的貼上,所以我們只需: ```cpp *dest++ = (original & mask) | (data >> write_lhs); ``` `(original & mask)` 讓左邊保持原本的狀態,右邊清成 0,接著透過 `|` 把右邊清掉的位元填上 `data`。 ```cpp _count -= bitsize; ``` 最後,要更新還需要寫幾個 bit。 ## 延伸問題 ### 改善 bit copy 的效能 原本的版本讓人詬病的地方是做了太多的檢查。當讀 `source` 時,為了能讓 `data` 存滿 8 bit ,會判斷一次,處理跨位址的位元。此外,複製到 `dest` 也會透過判斷來處理跨位址的貼上。而最糟糕的部份是,迭代的每一次都會做上述的判斷。 若我們要提高複製位元的效能,在原版本的基礎上,我們要做的是儘量降低判斷 (branch) 的次數。我想到一個方法是,一開始先處理最前頭小於 8 bit 的資料,接著而來的是一堆連續需要改動的 8 bit 資料,我們可以透過迴圈解決,最後處理最後一段小於 8 bit 的資料。以下為優化後的 `my_bitcpy` 片段程式碼: ```cpp const char MASK[8] = { 1, 3, 7, 15, 31, 63, 127, 255 }; uint8_t *src = (uint8_t *)_src + (_read / 8); uint8_t *dest = (uint8_t *)_dest + (_write / 8); uint8_t src_off = _read & 7; uint8_t dest_off = _write & 7; size_t offset = src_off - dest_off; int iter_time; uint8_t data; uint8_t mask = MASK[offset - 1]; int count = (int)_count; data = (*src++ << offset) | ((*src >> (8 - offset)) & mask); data &= MASK[8 - dest_off - 1]; *dest++ |= data; count -= 8 - dest_off; iter_time = count >> 3; while (--iter_time >= 0){ *dest++ = (*src++ << offset) | ((*src >> (8 - offset)) & mask); } int append = (count & 7); if (append > 0) { *dest |= ((*src++ << offset) | (*src >> (8 - offset))) & (~MASK[8 - append - 1]); } ``` 誠如前面的說明,一開始透過 bit operation,取出最前頭小於 8 bit 的資料存到 `data`,再貼上到 `dest`。接著是迴圈處理連續的 8 bit 貼上,這是也讓效能提昇最關鍵的地方,我們只需要判斷迭代次數就好,準備要貼上的東西有固定的模式,不須再額外判斷,而次數由 `count` 決定 ( 除以 8 ,因為一次 8 bit )。最後記得貼上剩下少於 8 bit 的尾巴。 以上的程式碼是建立在 `source_offset > dest_offset` 的情況寫的演算法。大於和等於的情況也是比照辦理,大概照著以下架構: ```cpp if (_read == _write) { .. } else if (_read > _write) { .. } else { .. } ``` 而我的演算法純粹以跨位址的複製為考量點,並未考量在同一個位址內的複製,因為在小範圍的複製,演算法對效能的影響有限。如同在 N 很小的條件下,複雜度 $O(N^2)$ 的 insertion sort 不會表現的比 $O(NlogN)$ 的 merge sort 還差。 ### 實驗 設計實驗,同樣以跨位址複製為出發點。評估當複製的位元數從 8 ~ 7900 兩種演算法 (優化前後) 的速度。 ```cpp static uint8_t output[1000], input[1000]; for (int size = 8; size < 7900; size++) { memset(&output[0], 0x00, sizeof(output)); clock_gettime(CLOCK_REALTIME, &t1); bitcpy(&output[0], 2, &input[0], 4, size); clock_gettime(CLOCK_REALTIME, &t2); printf("%d %ld",size,elapse(t1, t2)); memset(&output[0], 0x00, sizeof(output)); clock_gettime(CLOCK_REALTIME, &t1); my_bitcpy(&output[0], 2, &input[0], 4, size); clock_gettime(CLOCK_REALTIME, &t2); printf(" %ld\n",elapse(t1, t2)); printf("\n"); } ``` 以下為實驗結果 (已排除干擾): ![](https://i.imgur.com/cBYp4oH.png) 由上面的結果可以觀察到,如同我一開始的說法,當複製的位元數很小,其實演算法對效能的影響有限。反之,當 N 上升到一定的程度,便可看出速度的差異,當 N = 7900 時,我的演算法比原本的快 3 倍。 另外還可以透過 `perf` 來觀察是否有達到一開始的目標, branch 數量的減少。 ```shell $ sudo perf record \ -e branch-misses:u,branch-instructions:u \ -F 11000 ./cpybit $ sudo perf report ``` 可以觀察到當執行 `bitcpy` 時: ```shell Available samples ------------------------------- 293 branch-misses:u 489 branch-instructions:u ``` 當執行 `my_bitcpy` 時: ```shell Available samples ------------------------------- 320 branch-misses:u 427 branch-instructions:u ``` 的確,修改後的版本 branch 指令有減少。 :::warning 可對照 Linux 核心的 [bitmap](https://www.kernel.org/doc/htmldocs/kernel-api/kernel-lib.html) 實作,做了進一步的包裝,注意到 `IS_ALIGNED` 和 `__builtin_constant_p` 的使用 > 原始程式碼: [include/linux/bitmap.h](https://github.com/torvalds/linux/blob/master/include/linux/bitmap.h) :notes: jserv ::: ### 補充 透過老師的提示,我找到 bitmap.h 中的 `bitmap_set`,觀察 linux 專案如何實作未對齊字元的操作。 ```cpp static __always_inline void bitmap_set(unsigned long *map, unsigned int start, unsigned int nbits) { if (__builtin_constant_p(nbits) && nbits == 1) __set_bit(start, map); else if (__builtin_constant_p(start & BITMAP_MEM_MASK) && IS_ALIGNED(start, BITMAP_MEM_ALIGNMENT) && __builtin_constant_p(nbits & BITMAP_MEM_MASK) && IS_ALIGNED(nbits, BITMAP_MEM_ALIGNMENT)) memset((char *)map + start / 8, 0xff, nbits / 8); else __bitmap_set(map, start, nbits); } ``` `bitmap_set` 主要分成三個功能: * 若需要 set 的 bit 數只有 1 個,則直接呼叫 `__set_bit` * 若需要 set 的 bits 的起始位置和終了位置都有對齊,則可直接用 `memset` 來完成 * 若需要處理沒對齊的位元操作則呼叫 `__bitmap_set`。 很明顯地,我們在乎的是最後一個功能,看看原始碼如何處理非對齊的位元操作,於是我們往 `__bitmap_set` 裡面看: ```cpp void __bitmap_set(unsigned long *map, unsigned int start, int len) { unsigned long *p = map + BIT_WORD(start); const unsigned int size = start + len; int bits_to_set = BITS_PER_LONG - (start % BITS_PER_LONG); unsigned long mask_to_set = BITMAP_FIRST_WORD_MASK(start); while (len - bits_to_set >= 0) { *p |= mask_to_set; len -= bits_to_set; bits_to_set = BITS_PER_LONG; mask_to_set = ~0UL; p++; } if (len) { mask_to_set &= BITMAP_LAST_WORD_MASK(size); *p |= mask_to_set; } } ``` 其實以上的實作和我提供的方法很像,都是只處理前後兩段非對齊的記憶體 + 用迭代處理中間連續的記憶體。簡單說明一下 `__bitmap_set`,運作原理。`bits_to_set` 是紀錄從 `start` 到下一個 unsigned long 前還需要設幾個 bit。然後 `mask_to_set` 就是等等要用來 set 第一段 ( 非對齊 ) 的遮罩。接著進入迴圈就是一次 set 整個 unsigned long,直到需要 set 的 bit 數量小於一個 unsigned long 的長度。跳出迴圈之後,做和一開始類似的事情,但只是這次是從 unsigned long 的頭開始 set。 :::info 提問: 還是不太理解為甚麼除了需要 IS_ALIGNED 之外 還需要 __builtin_constant_p 來判斷變數是否在編譯時期是常量 :8ball: eecheng87 ::: --- ## 題目 `2` ### 解釋程式運作原理 #### 巨集 ( 挑幾個特別的說明 ) * `NEXT_POWER_OF_2` 如同名字所指,巨集的功能是在找大於傳入參數的最小的 $2^k$。將舉 2 個例子來解釋運作原理 : * 若 k 為 $2^n$,透過巨集得到 $2^n$,因為 $(2^k - 1)\ \& \ 2^k$ 一定會得到 0,所以回傳自己。 * 若 k 非 $2^n$,則表示為 $2^{64-builtin\_clzl(k)}$。 * `v(t, s, name, ...)` ...表示可以傳入多個參數,再由 `__VA_ARGS__` 取得值,詳細用法可以參考[這裡](https://ithelp.ithome.com.tw/articles/10160393)。另外,`__attribute__((cleanup(vec_free)))`的用法表示當被形容的變數離開 block 時,會呼叫 `vec_free`,而參數是該變數的指標型態。 * `NON_NULL __attribute__((nonnull))` 確保被此特性描述的指標型態參數不為 NULL。 > [GCC Manual: Declaring Attributes of Functions](https://gcc.gnu.org/onlinedocs/gcc/Function-Attributes.html) ) :::warning GCC 的 nonnull 屬性對於編譯器施加最佳化有益,可見 [The GCC nonnull attribute and compiler optimizations](http://rachid.koucha.free.fr/tech_corner/nonnull_gcc_attribute.html) :notes: jserv ::: :::danger 給定的 vector 實作存在若干嚴重疏失,例如: 1. `vec_pop_back` 沒進行例外處理; 2. 巨集 `v` 並未對給定的型態進行檢查,至少需要排除 `void` 和 `void *` 3. 記憶體配置策略不可靠; 你應該指出並討論改進方案。 :notes: jserv ::: ## 延伸問題 ### 參照 `fbvector` #### 記憶體配置策略 研讀 [folly/FBVector 文件](https://github.com/facebook/folly/blob/master/folly/docs/FBVector.md),發現要優化 vector 的第一個步驟是替換掉原本 realloc 的大小,原版本 vector 一次會給現在大小的兩倍 (即 factor 為 `2`)。但是這樣的壞處是,每次 realloc 新的一塊總是會比以前 alloc 的總和還小,因為目前所有 alloc 的大小 $c\sum_{i = 0}^{n} 2^{i} = c\ (2^{n+1}-1)$ ,一定會小於接著要 realloc 的大小 $c \times 2^{n+1}$。 fbvector 選的 factor 是 1.5,他可以在第五次 realloc 時,重複利用之前 alloc 的記憶體。 ```= k = 1.5, c = 4 0123 012345 012345678 0123456789ABCD 0123456789ABCDEF0123 0123456789ABCDEF0123456789ABCD 0123456789ABCDEF0123456789ABCDEF ``` 可以注意到第 6 行可以被前幾次已經拿到的記憶體容納下。 #### The jemalloc Connection fbvector 會透過演算法判斷當下應該新配置多少記憶體 :::warning 可對照 [Deterministic Memory Allocation for Mission-Critical Linux](https://static.sched.com/hosted_files/ossna2017/cf/Deterministic%20Memory%20Allocation%20for%20Mission-Critical%20Linux.pdf),在 2017 年我們進行一些實驗,記憶體配置的情境和對應策略組合,有著巨大的差異。 :notes: jserv ::: ### 新增 vector 經典操作 #### insert ```cpp #define vec_insert(v, e, pos) \ __vec_insert(&v, &(__typeof__(v.buf[0])[]){e}, pos, vec_elemsize(v),\ vec_capacity(v)) static NON_NULL void __vec_insert(void *restrict vec, void *restrict e, size_t pos, size_t elemsize, size_t capacity) { union { STRUCT_BODY(char); struct { size_t filler; char buf[]; }; } *v = vec; if (v->on_heap) { if (v->size == capacity) v->ptr = realloc(v->ptr, elemsize * (size_t)1 << ++v->capacity); memcpy(&v->ptr[(pos + 1) * elemsize], &v->ptr[pos * elemsize], elemsize * ((v->size++) - pos)); memcpy(&v->ptr[pos * elemsize], e, elemsize); } else { if (v->size == capacity) { void *tmp = malloc(elemsize * (size_t) 1 << (v->capacity = capacity + 1)); //memcpy(tmp, v->buf, elemsize * v->size); memcpy(tmp, v->buf, elemsize * pos); memcpy(tmp + (pos + 1) * elemsize, &v->buf[pos + 1], elemsize * ((v->size++) - pos)); v->ptr = tmp; v->on_heap = 1; memcpy(&v->ptr[pos * elemsize], e, elemsize); } else{ memcpy(&v->buf[pos * elemsize], e, elemsize); v->size++; } } } ``` 其實 insert 需要考慮的事和 `push_back` 幾乎一樣,我們需要先判斷出目前的 vector 在 heap 或是 stack,接著在判斷是否有足夠的空間塞入新元素。但是 insert 需要額外處理新元素的位置。我採用的方式是用 `memcpy` 直接把要插入的點以後的所有成員往後移一格,這種方式的優點是不用像最直覺的方式,一個成員一個一個移。不過這種方式會帶來的壞處就是隨著插入的位置越前面,你所需 copy 的成員將會越多,意味著越慢。 ##### 實驗 簡單做個實驗,將新成員插入 0~9999 的位置,分別紀錄時間。嘗試應證上方所言,插入的點越前面,速度越慢: ```cpp // experiment v(char, 3, v1); for (int i = 0; i < 10000; i++) vec_push_back(v1, 98); for(int i = 0; i < 10000; i++){ clock_gettime(CLOCK_REALTIME, &t1); vec_insert(v1,'a',i); clock_gettime(CLOCK_REALTIME, &t2); printf("%d %ld\n",i,elapse(t1,t2)); vec_erase(v1,i); } ``` 透過 gnuplot 作圖: ![](https://i.imgur.com/0twCTf8.png) 整體來說符合預期,速度和位置大小成反比。 #### erase 這個操作同樣也需要位移一堆連續的成員,已達到刪除的效果,最後記得更新變數 `size`。 ```cpp if (v->on_heap) { memcpy(&v->ptr[pos * elemsize], &v->ptr[(pos + 1) * elemsize], (v->size - pos - 1) * elemsize); } else { memcpy(&v->buf[pos * elemsize], &v->buf[(pos + 1) * elemsize], (v->size - pos - 1) * elemsize); } v->size--; ``` ![](https://i.imgur.com/8VPWeIn.png) 同樣也可觀測到,刪除的位置和時間有關係。 另外,我們再來觀察看看若用 C++ 標準函式庫提供的 [std::vector](https://en.cppreference.com/w/cpp/container/vector) 來刪除成員,是否也會和我們實做的產生類似的結果。 #### 實驗 ```cpp vector <char> v1; struct timespec t1, t2; for(int i = 0; i < 10000; i++) v1.push_back('b'); for(int i = 0; i < 10000; i++){ v1.insert(v1.begin(),'a'); clock_gettime(CLOCK_REALTIME, &t1); v1.erase(v1.begin()+i); clock_gettime(CLOCK_REALTIME, &t2); cout<<i<<" "<<elapse(t1,t2)<<"\n"; } ``` ![](https://i.imgur.com/7viedQA.png) 看來 c++ 刪除元素的速度也是會受到位置的影響。不過我在這裡偶然注意到一件事,似乎 c++ 在同樣操作的速度下和 c 相比,慢蠻多的。於是,我進一步的來驗證看看。 接著我們再設計一個實驗,分別用 C 和 C++ 來執行四種操作一千次,分別是 `push_back`, `insert`, `erase` 和 `pop_back`。然後紀錄每次操作的時間,程式碼如下: ```cpp for (int i = 0; i < 1000; i++){ clock_gettime(CLOCK_REALTIME, &t1); v1.push_back('b'); clock_gettime(CLOCK_REALTIME, &t2); cout << i << " " << elapse(t1, t2) << "\n"; } .. for(int i = 3000; i < 4000; i++){ clock_gettime(CLOCK_REALTIME, &t1); v1.pop_back(); clock_gettime(CLOCK_REALTIME, &t2); cout << i << " " << elapse(t1, t2) << "\n"; } ``` 將結果用 gnuplot 展示: ![](https://i.imgur.com/CTWkzXI.png) 很明顯的可以看出,C 版本 (我改進的實做) 的 insert 和 erase 速度比 C++ 標準函式庫提供的 [std::vector](https://en.cppreference.com/w/cpp/container/vector) 還快。接著把另外兩個操作放大來看。 ![](https://i.imgur.com/zoxREiU.png) 也可以發現 C 版本的操作會比較快。 :::warning TODO: 研讀 Linux 核心原始程式碼 [flexible arrays](https://www.kernel.org/doc/Documentation/flexible-arrays.txt) 的設計和實作 :notes: jserv ::: ### 補充 因老師要求,開始研讀 [flexible arrays](https://www.kernel.org/doc/Documentation/flexible-arrays.txt) 的設計和實作。 首先在 `flex_array.h` 中觀察結構 `flex_array`: ```cpp struct flex_array { union { struct { int element_size; int total_nr_elements; int elems_per_part; u32 reciprocal_elems; struct flex_array_part *parts[]; }; char padding[FLEX_ARRAY_BASE_SIZE]; }; }; ``` 除了一些功能很明顯的變數外,`padding` 主要的功能是結合 union 特性,來限制這個結構大小在 1 個 page。 而因為 `flex_array` 中的 flex,所以我猜這個陣列應該有類似 vector 能自動伸縮的功能,接著打開 .c 檔案來看各種操作的實作。 首先看到函數 `flex_array_alloc`: ```cpp struct flex_array *ret; int max_size = FLEX_ARRAY_NR_BASE_PTRS * FLEX_ARRAY_ELEMENTS_PER_PART(element_size); /* max_size will end up 0 if element_size > PAGE_SIZE */ if (total > max_size) return NULL; ret = kzalloc(sizeof(struct flex_array), flags); if (!ret) return NULL; ret->element_size = element_size; ret->total_nr_elements = total; if (elements_fit_in_base(ret) && !(flags & __GFP_ZERO)) memset(&ret->parts[0], FLEX_ARRAY_FREE, FLEX_ARRAY_BASE_BYTES_LEFT); return ret; ``` 這個函數在做的事基本上就是重新拿一塊新的 `flex_array` 然後把成員變數填一填。這裡面值得注意的是 `kzalloc`,這是在 kernel mode 下請求記憶體需要呼叫的函數。和 `vmalloc` 的差異是在前者只能拿 1 個 page 以內的記憶體,超過就要用後者。 接著來看 `flex_array_put`: ```cpp int part_nr = fa_element_to_part_nr(fa, element_nr); struct flex_array_part *part; void *dst; if (element_nr >= fa->total_nr_elements) return -ENOSPC; if (elements_fit_in_base(fa)) part = (struct flex_array_part *)&fa->parts[0]; else { part = __fa_get_part(fa, part_nr, flags); if (!part) return -ENOMEM; } dst = &part->elements[index_inside_part(fa, element_nr)]; memcpy(dst, src, fa->element_size); return 0; ``` 這個函數的功能有點像 vector 的 push。把你需要加入的資料 ( src ) 插入 flex_array 中。如果 src 的大小沒有超過 array ( 第 2 個 if statement ),理所當然可以塞入,但是我真正關心的是另外一種狀況,我想知道所謂的 **flex** array 當塞入超過原本範圍限制的資料會做甚麼應對。 首先解釋第 1 個 if statement,可以發現 `if (element_nr >= fa->total_nr_elements)` 就是當 src 大小超過 array 的邊界 ( 無法 append ),條件為真,會回傳一個 error code。把問題丟回去給上層使用者 ( 呼叫 put 的人 ),因為這是一個不合理的呼叫。 但反觀最後的 else block,這個是當要插入到目前最後一個資料的後面,會呼叫 `__fa_get_part`: ```cpp if (!part) { part = kmalloc(sizeof(struct flex_array_part), flags); if (!part) return NULL; if (!(flags & __GFP_ZERO)) memset(part, FLEX_ARRAY_FREE, sizeof(struct flex_array_part)); fa->parts[part_nr] = part; } ``` 而 `__fa_get_part` 中果然幫我們新拿了一塊記憶體 (kmalloc),做了伸長的動作 (flexible)。 提問: 1. 除了可以 shrink 和可以做類似 push_back() 功能之外,這樣和一般的 array 對比之下還有其他能稱得上 flex 的地方嗎 ? 2. 我搜尋過 linux 專案底下,沒找到有呼叫過這個函數的檔案,在 google 上也沒找到使用的實例。flex_array 真的有存在的必要?那會是怎麼樣的情境? :::info [Open vSwitch](https://www.openvswitch.org/) 用到 flex_array,見 [datapath: remove flex_array](https://mail.openvswitch.org/pipermail/ovs-dev/2016-July/319379.html) (2016): > "flex_array is already present on Linux 3.10, so there is no need for its backport anymore." 但之後就移除了,見 [datapath: convert to kvmalloc](https://lists.linuxfoundation.org/pipermail/ovs-dev/2019-March/357628.html) (2019) > "There was no real need for this code to be using flexarrays, it's just implementing a hash table - ideally it would be using rhashtables, but that conversion would be significantly more complicated." 這個案例說明 Linux 核心 (及生態系統,如上述的 Open vSwitch) 一直反思專案內部設計,不合時宜的設計會被取代。 也可嘗試對 LKML 提交改寫 flex_array 的修改。 :notes: jserv :::