--- tags: NCKU Linux Kernel Internals --- # quiz4 ## Material > * [2020q1 第 4 週測驗題](https://hackmd.io/@sysprog/linux2020-quiz4) > * [2021q1 第 2 週測驗題](https://hackmd.io/@sysprog/linux2021-quiz2) > * [Github](https://github.com/RinHizakura/bitcpy) ## bitcopy :::danger 注意到測驗題裡的原始程式似乎有寫錯的地方?後面會仔細討論此部份 ::: ### 實作原理 bitcpy 的用途是把 `_src` 從指定的位置 `_read` 開始, 從 `_dest` 的 `_write` 開始寫 `_count` 個 bit。 複製是 8 個 bits 為單位處理的,又因為允許不按照 alignment 存取,因此需要位移來處理。`read_lhs = _read & 7` 即是 `read_lhs = _read % 8`,如果我們要從 `_src` 中取出不對齊的資料,以下圖為例,如果目標是黃框中跨越兩個 bytes 的 8 bits。則讀出來步驟會是: * 先讀出 byte 1 到 `data`,然後 `data` 左移 5 bits * 讀出 byte 0 後右移 3 bits 後跟 `data` or 運算後存回 `data` 例子中的 5 bits / 3 bits 就是需要被推算的 `read_lhs`、`read_rhs`。write 的推算也是同理,因此 `KK1` = ( g )7 ![](https://i.imgur.com/yBmiL9X.png) 取出後,再透過 bitsize 推算出前幾位數需要被複製,到這步驟,loop 這一輪需要的 8 bits `data` 就被準備好了,這就是到下面一段程式所做的事。 ```c while (_count > 0) { data = *source++; bitsize = (_count > 8) ? 8 : _count; if (read_lhs > 0) { data <<= read_lhs; if (bitsize > read_rhs) data |= (*source >> read_rhs); } if (bitsize < 8) data &= read_mask[bitsize]; ... ``` 調整寫入位置的方法很類似,不過再稍微複雜一些。在 ` _dest ` 非 align 的情境下,有兩個狀況要考慮: * 如果要寫入 bit 數包含接續在後面的 bytes (`if (bitsize > write_rhs)`),則先把當前 bytes 的部分寫進去,再前進到下個 byte 補完。 * 需要寫入的量如果不包含後面的 bytes,把當前 bytes 的部分寫好就可以。 ```c original = *dest; if (write_lhs > 0) { mask = read_mask[write_lhs]; if (bitsize > write_rhs) { *dest++ = (original & mask) | (data >> write_lhs); original = *dest & write_mask[bitsize - write_rhs]; *dest = original | (data << write_rhs); } else { if ((bitsize - write_lhs) > 0) mask = mask | write_mask[8 - (bitsize - write_lhs)]; *dest++ = (original & mask) | (data >> write_lhs); } } ``` `_dest` 是 align 的話,如果要寫 8 bits 直接放進去,如果少於 8 bits 就把要寫的地方用 mask 清掉,再把 `data` 放進去即可。 ```c else { if (bitsize < 8) data = data | (original & write_mask[bitsize]); *dest++ = data; } _count -= KK2; } ``` 因為每一次都處理掉 bitsize 大小的數據,因此 `KK2` = ( d ) bitsize。 ### 改寫為更精簡且等效的程式碼 #### 錯誤? 程式的此部份邏輯有點奇怪: ```c else { if ((bitsize - write_lhs) > 0) mask = mask | write_mask[8 - (bitsize - write_lhs)]; *dest++ = (original & mask) | (data >> write_lhs); } ``` 這個 else 的目的應該是當 bitsize 小於 `write_rhs` 時,要避免把 dest 的部份覆蓋掉而去調整 mask,因此感覺應該要修正成: ```c else { mask |= write_mask[8 - (write_rhs - bitsize)]; *dest++ = (original & mask) | (data >> write_lhs); } ``` 又 `8 - write_rhs` 實際上就是 `write_lhs`,所以可以等價修正成 ```c else { mask |= write_mask[write_lhs + bitsize)]; *dest++ = (original & mask) | (data >> write_lhs); } ``` 同時也透過概念相似的 python code 來檢驗結果(用一個 int 來存 1 bit,所以比較容易撰寫,也因此應該不太會出錯,詳細請參考 Github)。如果沒有誤會 `bitcpy` 的功能的話,把輸出結果用 `diff` 來檢查,更改後的結果應該才是正確的。 :::warning 因為測驗題原本給程式的是全零的 `output` 和全一的 `input`,某些程度上錯誤的程式也可能導致正確的結果(因為 pattern 太單一),所以在檢驗正確性的時候需要小心。 ::: #### 進一步簡化程式 確認了正確的結果為何後,我們就可以放心的調整程式,再檢驗輸出來確定結果是否正確。 通過把 special case 融合成 general case,修改後的程式為: ```c .../* */ while (_count > 0) { data = *source++; bitsize = (_count > 8) ? 8 : _count; data <<= read_lhs; data |= (bitsize > read_rhs) ? (*source >> read_rhs) : 0; data &= read_mask[bitsize]; original = *dest; int idx = bitsize > write_rhs ? 8 : 8 - (write_rhs - bitsize); *dest++ = (original & (read_mask[write_lhs] | write_mask[idx])) | (data >> write_lhs); idx = bitsize < write_rhs ? 0 : bitsize - write_rhs; *(dest) = (*dest & write_mask[idx]) | (data << write_rhs); _count -= bitsize; } ``` :::warning 雖然我知道 `? : ` 不會減少分支,不過至少讓程式看起來乾淨一點...嗎? ::: ### 改寫為執行效率更高的程式碼 原始的程式為了可以做到儘量以最大 8 bits 來操作,因此需要繁瑣的 branch ,且沒有很好的考慮到 align 問題導致需要一直在非 align 的狀況下處理 bitstring。 修改後的程式如下,其中 `bitcpy_naive` 就是前面改為更精簡的版本。我們的方法其實是把 bitcpy 切成3個部份: * 一開始先檢查 dest 是否有 align 好,如果沒有就先只處理沒 align 的第 1 部份 * dest 從有 align 的地方開始處理,直到最後一個不到 8 bits 的部份 * 把最後的部份 copy 完 注意到程式用比較簡單的方式設計,透過呼叫可以處理所有狀況的 `bitcpy_naive` 把特例處理掉讓程式撰寫比較簡單,但如果考慮只針對 special case 的話應該可以簡化某些運算避免使用 general 的方法。(但是我爛,寫出來都怪怪的QQ ,所以暫時以這種比較笨的方法) ```c if (write_lhs != 0 && _count >= write_rhs) { bitcpy_naive(_dest, _write, _src, _read, write_rhs); if (write_rhs > read_rhs) source++; _write += write_rhs; _read += write_rhs; _count -= write_rhs; dest++; read_lhs = _read & 7; read_rhs = 8 - read_lhs; /* No update is ok because we dont need this after write_lhs = _write & 7; write_rhs = 8 - write_lhs; */ } int iter_cnt = _count >> 3; int iter = _count >> 3; _count &= 7; while (--iter_cnt >= 0) { *dest++ = (*source << read_lhs) | ((*(source + 1) >> read_rhs)); source++; } bitcpy_naive(_dest, _write + (iter << 3), _src, _read + (iter << 3), _count); ``` ![](https://i.imgur.com/peTb4CW.png) 可以看到考慮到 align 讓執行的時間有明顯的減少。 ### Linux 核心中的 bitcpy - [ ] 觀察 [linux/bitops.h](https://github.com/torvalds/linux/blob/master/tools/include/linux/bitops.h) 裡頭巨集在原始程式碼的運用狀況 ## vector VV1 = ( d ) `(s & (s - 1)) == 0` VV2 = ( a ) `v.size -= 1` ### 實作原理 #### `v` ```c= #define v(t, s, name, ...) \ union { \ STRUCT_BODY(t); \ struct { \ size_t filler; \ t buf[NEXT_POWER_OF_2(s)]; \ }; \ } name __attribute__((cleanup(vec_free))) = {.buf = {__VA_ARGS__}}; \ name.size = sizeof((__typeof__(name.buf[0])[]){0, __VA_ARGS__}) / \ sizeof(name.buf[0]) - 1; \ name.capacity = sizeof(name.buf) / sizeof(name.buf[0]) ``` * 在 Union 中有兩種 struct。一種是透過 `STRUCT_BODY` 定義的。一種是有一個 `size_t` 的成員 `filter`,和 `NEXT_POWER_OF_2(s)` 大小的 `t` 型態 array。 * 根據 [cleanup (cleanup_function)](https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-Attributes.html) `__attribute__((cleanup(vec_free)))` 會在變數離開 stack 時自動呼叫 `vec_free`。 * 根據 [Variadic Macros](https://gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html),把 `name` 以後的參數對應到 `__VA_ARGS__`,用來初始化 `buf` 裡的內容。 * `size` 計算 vector 目前儲存的數量,透過 `(__typeof__(name.buf[0]))` 得到 buf 的型別,再用此型別去建立一個成員有 `{0, __VA_ARGS__}` 的 literal,則 vector 大小即是這個 literal 需要的儲存空間除以單個元素的儲存空間再減 1(扣掉 0) * 需要多放一個 0,是避免沒有 `__VA_ARGS__` 時 buf[0] 不會出錯 * `capacity` 是 vector 可以容納的數量 #### `STRUCT_BODY` 這個 macro 用來宣告 struct 結構,其中 `type` 是結構中 pointer 的型態。前面的 size_t 則是透過在 [C 語言:bit-field](https://hackmd.io/@RinHizakura/Hk1CYv1kv/%2F5B7s4KiaQOmS0Ud6Px6Cww) 提到的技巧,把 `size_t` 的 64 位元分配給不同的 struct member。 ```c= #define STRUCT_BODY(type) \ struct { \ size_t size : 54, on_heap : 1, capacity : 6, flag1 : 1, flag2 : 1, \ flag3 : 1; \ type *ptr; \ } ``` #### `NEXT_POWER_OF_2(s)` ```c= #define NEXT_POWER_OF_2(s) \ VV1 \ ? s \ : (size_t) 1 << (64 - __builtin_clzl(s)) /* next power of 2 */ ``` 求出比 `s` 大且最接近 `s` 的 2^n 次方(n 為整數)。因此 VV1 應該是 ( d )`(s & (s - 1)) == 0` (可以參閱 [C 語言:數值系統篇](https://hackmd.io/9tGfpJtLTwyyOzDvlyJS2w?view#x-amp-x---1--0)),如果 s 本身就是 2^n 次方則直接是 s,否則透過 `64 - __builtin_clzl` 求出 s 從左邊數來第 1 個 1 在哪,位移那個量可以得到所求。 #### `NON_NULL` ```c= #define NON_NULL __attribute__((nonnull)) ``` [__attribute__((nonnull))](https://www.keil.com/support/man/docs/armcc/armcc_chr1359124976631.htm) 告訴編譯器這個 function 不能接受 NULL pointer,否則會在編譯時期產生警告。 #### `vec_free` ```c= static NON_NULL void vec_free(void *p) { STRUCT_BODY(void) *s = p; if (s->on_heap) free(s->ptr); } ``` 檢查 vector 是否是用 heap 來儲存,是的話要透過 free 來釋放。 #### `vec_size` ```c= #define vec_size(v) v.size ``` 如你所見,不解釋。 #### `vec_capacity` ```c= #define vec_capacity(v) \ (v.on_heap ? (size_t) 1 << v.capacity : sizeof(v.buf) / sizeof(v.buf[0])) ``` 檢查 vector 是否額外使用 heap 儲存資料,如果是,$2^{v.capacity}$ 為實際的容量大小,否則就是 `sizeof(v.buf) / sizeof(v.buf[0]))`。 #### `vec_data` ```c= #define vec_data(v) (v.on_heap ? v.ptr : v.buf) /* always contiguous buffer */ ``` 針對是否使用 heap 存取的 member 不同。 #### `vec_elemsize` ```c= #define vec_elemsize(v) sizeof(v.buf[0]) ``` 如你所見。 #### `vec_pos` ```c= #define vec_pos(v, n) vec_data(v)[n] /* lvalue */ ``` 用來取 vector 的第 n 個值 #### `vec_reserve` ```c= #define vec_reserve(v, n) __vec_reserve(&v, n, vec_elemsize(v), vec_capacity(v)) ``` #### `__vec_reserve` ```c= static NON_NULL void __vec_reserve(void *vec, size_t n, size_t elemsize, size_t capacity) { union { STRUCT_BODY(void); struct { size_t filler; char buf[]; }; } *v = vec; if (n > capacity) { if (v->on_heap) { v->ptr = realloc(v->ptr, elemsize * (size_t) 1 << (v->capacity = ilog2(n))); } else { void *tmp = malloc(elemsize * (size_t) 1 << (v->capacity = ilog2(n))); memcpy(tmp, v->buf, elemsize * v->size); v->ptr = tmp; v->on_heap = 1; } } } ``` 透過 heap 擴大 vector 的 capacity #### `ilog2` ```c= static inline int ilog2(size_t n) { return 64 - __builtin_clzl(n) - ((n & (n - 1)) == 0); } ``` 求出 k,k 滿足比 n 大且最接近的 $2^k$(k 為整數) #### `vec_push_back` ```c= #define vec_push_back(v, e) \ __vec_push_back(&v, &(__typeof__(v.buf[0])[]){e}, vec_elemsize(v), \ vec_capacity(v)) ``` #### `__vec_push_back` ```c= static NON_NULL void __vec_push_back(void *restrict vec, void *restrict e, 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[v->size++ * 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); v->ptr = tmp; v->on_heap = 1; memcpy(&v->ptr[v->size++ * elemsize], e, elemsize); } else memcpy(&v->buf[v->size++ * elemsize], e, elemsize); } } ``` push 進一個元素,必要的時候要增加 capacity。 #### `vec_pop_back` ```c= #define vec_pop_back(v) (void) (VV2) ``` 拿出一個元素,所以 `VV2` 是 (a )`v.size -= 1` ### 實作限制與改進方案 #### `vec_pop_back` ```c= #define vec_pop_back(v) assert(v.size > 0); v.size -= 1 ``` 原本的 `vec_pop_back` 僅僅是把 `size -= 1`,對於空的 vector 的 pop 應該要反映出問題,因此修改為此。 #### `vec_pos` 透過 assert 檢查超出邊界的存取,詳細請參閱 Github。 #### `v` 透過 [Static Assertion](https://en.wikichip.org/wiki/c/static_assertion) 避免初始化 void 型別的 vector,需注意到 static assertion 的檢查只能接受 [Constant Expressions](https://www.cs.auckland.ac.nz/references/unix/digital/AQTLTBTE/DOCU_066.HTM) 而不能執行 function,不過我們可以透過 GCC builtin 來完成字串比對。 ```c= _Static_assert(__builtin_strncmp(#t, "void", 4), "error type"); ``` 詳情請參閱 Github。 #### 記憶體配置 在 [folly::fbvector](https://github.com/facebook/folly/blob/master/folly/docs/FBVector.md) 提到記憶體配置原則的重要性。當 vector 需要更多空間時,一次 malloc(realloc) 要重新要多少空間是很關鍵的議題,如果要的太少,會導致頻繁呼叫 malloc 產生的時間成本,但是如果要的太多又會有空間浪費的問題。 空間的成長幅度稱為 growth factor,在原始程式中,growth factor 為 2,也就是當空間不足時,就把 heap 空間擴大成兩倍。但是在文章中指出此設計不好的地方。假如初始一塊大小為 C 的空間,則 growth factor = k,下一次需要的空間是原本的 K 倍: $C$, $C * k$, $C * k^2$, $C * k^3$, ... 在 k = 2 的狀況下,因為 $C$ + $C * k$ + $C * k^2$ + ... $C * k^{n-1}$ < $C * k^n$ 恆成立,因此每次重新配置的空間都比之前配置過的總和大,導致每一次配置就真的需要多要記憶體,而沒辦法重新利用之前配置的空間(如果新的所需空間總和小於之前,則有機會從 pool 裡拿出之前配置的空間來使用就足夠)。 需注意到 allocator 實際上是怎麼分配會影響到實作的策略,不過 k = 2 不論如何都是理論上最差的選擇。而 fbvector 與 jemalloc 搭配,使用 growth factor 1.5 得到最佳效能。 - [ ] 研究如何設計使用者定義的 allocator,搭配記憶體的配置策略得到更佳的效能 ### 實現經典 vector 操作 #### `front` / `back` ```c= #define vec_front(v) vec_pos(v, 0) #define vec_back(v) vec_pos(v, v.size-1) ``` 透過 `vec_pos` 去存取也可以確保錯誤使用時(例如空 vector 時用 `vec_front`)被 assert 擋下來。 #### `insert` C++ 中的 insert 根據參數功能會有點差異,其中最基本的功能是可以把指定的 element `e` 插入到 `p` 位置。 ```c= #define vec_insert(v, p, e) \ do { \ assert(p >= 0 && p < v.size); \ __typeof__(v.buf[0]) __tmp; \ for (int i = 0; i < (signed) v.size; i++) { \ if (i == p) { \ __tmp = vec_pos(v, i); \ vec_pos(v, i) = e; \ } else if (i > p) { \ __typeof__(v.buf[0]) __t; \ __t = __tmp; \ __tmp = vec_pos(v, i); \ vec_pos(v, i) = __t; \ } \ } \ vec_push_back(v, __tmp); \ } while (0) ``` ```c= #define vec_insert_n(v, p, n, e) \ do { \ assert(p >= 0 && p < v.size); \ assert(n > 0); \ __typeof__(v.buf[0]) *__tmp = \ malloc(sizeof(__typeof__(v.buf[0])) * (v.size - p)); \ int cnt = 0; \ for (int i = 0; i < (signed) v.size; i++) { \ if (i >= p) { \ __tmp[cnt++] = vec_pos(v, i); \ } \ } \ for (int i = 0; i < (signed) v.size - p; i++) \ vec_pop_back(v); \ vec_reserve(v, v.size + n + cnt); \ for (int i = 0; i < n; i++) \ vec_push_back(v, e); \ for (int i = 0; i < cnt; i++) \ vec_push_back(v, __tmp[i]); \ free(__tmp); \ } while (0) ``` 作法稍微有點暴力,尤其是 `vec_insert_n`,可以實作出相同效果的方法有許多種,需要考量到效率去做改進。 :::warning 雖然用 macro 可以減少轉型,讓對 vector 的操作比較簡單,不過也因此如果認真要做 assert 可以有很多需要檢查的(在 int 的 vector 插入 float element? `vec_insert_n` 如果 insert 的數量是 float?)。如果要增加實用性的話,需要對這些層面去改善(真的 assert 去檢查?或者換成 function 型式讓 compiler 去檢查)。 ::: #### erase