# 2023q1 Homework3 (quiz3) contributed by < `yanjiew1` > > [GitHub](https://github.com/yanjiew1/linux23q1-quiz3) > [題目連結](https://hackmd.io/@sysprog/linux2023-quiz3) ## 實驗環境 ```bash $ gcc --version gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0 $ lscpu 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): 8 On-line CPU(s) list: 0-7 Vendor ID: GenuineIntel Model name: Intel(R) Core(TM) i5-8250U CPU @ 1.60GHz CPU family: 6 Model: 142 Thread(s) per core: 2 Core(s) per socket: 4 Socket(s): 1 Stepping: 10 CPU max MHz: 3400.0000 CPU min MHz: 400.0000 BogoMIPS: 3600.00 ``` ## 測驗 1 可改進的地方: - 紅黑樹實作內部使用的函式除了宣告為`static` 外,也可以宣告為 `inline` ,來改進效能,因為函式呼叫有成本。例如: `cmap_rotate_left` 、 `cmap_rotate_right` 、 `cmap_l_l` 、 `cmap_l_r` 、 `cmap_r_l` 、 `cmap_r_r` 等。 :::warning 後來看了一下 Linux coding style , Linux 不鼓勵為了加速就把函式宣告為 `inline` 。只有大約二到三行的函式再宣告為 `inline` 就好。實際上 GCC 在沒有宣告 `inline` 的情況下,也會為了優化去自動 inline 一些函式。 ::: ## 測驗 3 ### Linear feedback shift register (LFSR) 的運作原理 在維基百科對 LFSR 有這樣的定義: ```! In computing, a linear-feedback shift register (LFSR) is a shift register whose input bit is a linear function of its previous state. ``` 故 LFSR 是一個在狀態改變時,會向右位移的 register ,向右位移時,最左邊的輸入位元,是由目前原本 register 內狀態取一些 bit 出來,並透過線性方式組合。 維基百科有下面這張圖: ![](https://i.imgur.com/GgDQpkW.png) 下一個狀態是前一個狀態每一個位元向右移,最左邊輸入的 bit ,是由原本狀態中第 11 、 13 、 14 、 16 個 bit 透過 XOR 運算得到的。 最左邊輸入位元怎麼組合沒有明確定義,只要是由原本狀態的線性組合即可。 參考資料: - [Wikipedia: Linear-feedback shift register](https://en.wikipedia.org/wiki/Linear-feedback_shift_register) ### 程式碼運作原理 程式會建立 120 個初始值為 0 的 bucket ,然後跑 $2^{20}$ 次迴圈,每次迴圈會透過 LFSR 從 0 ~ 119 任選一個隨機數作為 bucket 編號,把對應的 bucket 編號值加一。 一開始程式定義了一個 LFSR 的操作: ```c /* Implementation of LFSR (linear feedback shift register) * on uint64_t using irreducible polynomial x^64 + x^61 + x^34 + x^9 + 1 * (On 32 bit we could use x^32 + x^22 + x^2 + x^1 + 1) */ static void lfsr(uint64_t *up) { uint64_t new_bit = ((*up) ^ ((*up) >> 3) ^ ((*up) >> 30) ^ ((*up) >> 55)) & 1u; /* don't have to map '+1' in polynomial */ *up = ((*up) >> 1) | (new_bit << 63); /* shift *up by 1 to RIGHT and insert new_bit at "empty" position */ } ``` `new_bit` 就是在計算要放在最左邊的輸入位元。由程式可知,輸入位元由(由右往左數,最右邊為 0)第 0 、3、30、55 位元進行 XOR 運算組成。 再把 `*up` 向右移一位,把 `new_bit` 放在最左邊空出的一位,就完成了。 下面定義了一個表格 `log_table_256` ,其輸出數值為輸入 index 取 $\log_2$ 的值。為了避免重複填寫同一個數值,這裡還定義了一個巨集 `_` 巨集會把同一個數字複製 16 份,中間用 `,` 隔開。 ```c static const char log_table_256[256] = { #define _(n) n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n -1, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, _(4), _(5), _(5), _(6), _(6), _(6), _(6), _(7), _(7), _(7), _(7), _(7), _(7), _(7), _(7), #undef _ }; #undef _ ``` `log2_64` 函式是針對傳入的 64-bit 整數,計算其 $\log_2$ 的值,並取其整數部份。運作原理類似二分搜尋法,找到最高位的位置。 ```c /* ASSUME x >= 1 * returns smallest integer b such that 2^b = 1 << b is >= x */ uint64_t log2_64(uint64_t v) { unsigned r; uint64_t t, tt, ttt; ttt = v >> 32; if (ttt) { tt = ttt >> 16; if (tt) { t = tt >> 8; if (t) { r = 56 + log_table_256[t]; // AAAA } else { r = 48 + log_table_256[tt]; // BBBB } } else { t = ttt >> 8; if (t) { r = 40 + log_table_256[t]; // CCCC } else { r = 32 + log_table_256[ttt]; // DDDD } } } else { tt = v >> 16; if (tt) { t = tt >> 8; if (t) { r = 24 + log_table_256[t]; // EEEE } else { r = 16 + log_table_256[tt]; // FFFF } } else { t = v >> 8; if (t) { r = 8 + log_table_256[t]; // GGGG } else { r = 0 + log_table_256[v]; // HHHH } } } return r; } ``` 若搭配測驗 4 的 bitwise 操作,可簡化為 ```c uint64_t log2_64(uint64_t x) { uint64_t r, shift; r = (x > 0xFFFFFFFFL) << 5; x >>= r; shift = (x > 0xFFFF) << 4; x >>= r; r |= shift; shift = (x > 0xFF) << 3; x >>= shift; r |= shift; shift = (x > 0xF) << 2; x >>= shift; r |= shift; shift = (x > 0x3) << 1; x >>= shift; return (r | shift | x >> 1); } ``` 或是透過 `__builtin_clz64` 來加速: ```c uint64_t log2_64(uint64_t v) { return 63 - __builtin_clz64(v); } ``` --- `bucket_number` ,會透過傳入的隨機數 `x` 來選取一個 bucket 。 其中 `mask111` ,為二進位有效位數全為 1 且位元數為 bucket 位元數的整數。 `mask011` 則為二進位有效位數全為 1 且位元數為 bucket 位元數少 1 的整數。故可知道 `mask111 >= bucket number > mask011` 。 leq 則判斷對 `x & mask111` 是否小於 bucket 數。針對 `leq` 的結果,有以下產生的方式: - 若 `x & mask111` 小於 bucket 數 ,則結果為 `x & mask111` ,等價於 `x % N_BUCKETS` 。 - 若 `x & mask111` 大於 bucket 數,則結果為 `(x >> (N_BITS + 1)) & mask011` ,即取 `x` 的較高位數作為隨機數,然後再跟 `mask011` 做 AND 運算,確保取得的數小於 bucket 數值。 ```c /* n == number of totally available buckets, so buckets = \{0, ...,, n-1\} * ASSUME n < (1 << 32) */ unsigned int bucket_number(uint64_t x) { uint64_t mask111 = (1 << (N_BITS + 1)) - 1; uint64_t mask011 = (1 << (N_BITS)) - 1; /* one 1 less */ unsigned char leq = ((x & mask111) < N_BUCKETS); /* leq (less or equal) is 0 or 1. */ return (leq * (x & mask111)) + // IIII ((1 - leq) * ((x >> (N_BITS + 1)) & mask011)); // JJJJ /* 'x >> (N_BITS + 1)' : take different set of bits -> better uniformity. * '... & mask011' guarantees that the result is less or equal N_BUCKETS. */ } ``` 雖然註解中這樣寫 `'x >> (N_BITS + 1)' : take different set of bits -> better uniformity.` ,若看程式執行的結果,產生的隨機數還是會有偏差。前 64 個 bucket 明顥比較多次被選中。 --- `fill_buckets` 會跑 `1 << 20` 次迴圈(可以從 `main` 函式得知傳入的 `iterations` 值),每一次透過 `bucket_number` 選取一個 bucket ,把選中的 bucket 數值加 1 。之後透過 LFSR ,產生下一組隨機數,再重複進行。 ```c void fill_buckets(unsigned int *buckets, unsigned int iterations) { uint64_t x = 0x98765421b16b00b5; unsigned char lfsr_iter = (N_BITS << 1); for (uint64_t i = 0; i < iterations; i++) { unsigned int tmp_bucket = bucket_number(x); *(buckets + tmp_bucket) = *(buckets + tmp_bucket) + 1; // 'turn handle' on LFSR lfsr_iteration-times! unsigned char ell = 0; while (ell < lfsr_iter) { lfsr(&x); ell++; } } } ``` --- ## 測驗 4 ### 原理解說 原理是類似 binary search 的方式實作。因為要取 $\lceil log_2(x) \rceil$ ,故一開始先對 `x` 減去 1 ,就能直接用 $\lfloor log_2(x - 1) \rfloor$ 二進位的值,算出 $\lceil log_2(x) \rceil$ 的值。 程式中,使用 `r` 作為記錄總共向右移的 bits 數, `shift` 則是暫存要向右移多少 bits 的結果。 這段程式會先判斷 `x` 是否大於 `0xFFFF` ,若大於 `0xFFFF` 則代表其最高位在第 `31 ~ 16` bit 之間,則需向右移 16 bits ,把接下來要檢索的部份放到右邊的 0~15 bits 之間。 在這裡 `r` 記錄向右移動的次數,用來推算最後 $\lceil log_2(x) \rceil$ 的值。 ```c r = (x > 0xFFFF) << 4; x >>= r; ``` 前面已經把檢索範圍縮小到 16-bits 。`x > 0xFF` 成立時,代表最高位在 8 ~15 bits 之間,則要向右移 8 個 bits 。 `shifts` 的值就是要向右移的 bits 數,而 `r` 則記錄著總共向右移的次數, `r |= shift;` 會把先前向右移的次數和這次向右移的次數加起來。這裡用 `|` 是因為 `r` 是 1 的 bits 不會和 `shift` 重疊。 ```c shift = (x > 0xFF) << 3; x >>= shift; r |= shift; ``` 檢索範圍已縮小到 0 - 7 bits 。 `x > 0xF` 成立時,代表最高位在 4 ~ 7 bits 之間,則要再向右移 4 個 bits 。 `shift` 的值是要向右移的 bits 數。 `r` 則把先前已經向右移的 bits 數和現在的 `shift` 加總,為至目前為止向右移的 bits 數。 ```c shift = (x > 0xF) << 2; x >>= shift; r |= shift; ``` 檢索範圍已縮小到 0 - 3 bits。 `x > 0x3` 成立時,代表最高位在 2 ~ 3 bits 之間,則要向右移 2 個 bits 。 `shift` 的值是要向右移的 bits 數。 ```c shift = (x > 0x3) << 1; x >>= shift; ``` 檢索範圍已縮小到 0 - 1 bits 了。而前面因為 `r` 沒有跟 `shift` 做 bitwise or 運算,把向右移的次數加總,故在 return 時,要先讓 `r` 跟 `shift` 做 bitwise or 運算。而 `x >> 1` 則是當最高位是在第 1 位時,需要再多加上這一位數。最後 `+ 1` 是因為我們要取的是 $\lceil log_2(x) \rceil$ ,即取上界,故還要再多加 1 。 ```c return (r | shift | x >> 1) + 1; ``` ### 讓上述程式支援 `x == 0` 的情況 由於 $\log_2 0$ 結果未定義,故我們定義此函式處理 `x` 為 0 時,輸出為 0 。 程式一開始先判斷是否為 0 ,把判斷結果存到 `zero`。 最後回傳結果時,把輸出結果乘上 `!zero` ,即若 `x` 為 0 時,則輸出結果會乘上 0 ,否則乘上 1 ,就可以確保在 `x` 為 0 時,輸出為 0 。 這樣子就可以達到沒有分支,又可以正確處理 `x == 0` 的情況。 ```c int ceil_log2(uint32_t x) { uint32_t r, shift; int zero = (x == 0); x--; r = (x > 0xFFFF) << 4; x >>= r; shift = (x > 0xFF) << 3; x >>= shift; r |= shift; shift = (x > 0xF) << 2; x >>= shift; r |= shift; shift = (x > 0x3) << 1; x >>= shift; return ((r | shift | x >> 1) + 1) * !zero; } ``` ### Linux 核心內 ceil 和 log2 的組合,及應用