--- tags: linux2022 --- # 2022q1 Homework (quiz2) contributed by <`ibat10clw`> > [quiz2](https://hackmd.io/@sysprog/linux2022-quiz2#2022q1-%E7%AC%AC-2-%E9%80%B1%E6%B8%AC%E9%A9%97%E9%A1%8C) ## 測驗1 ### 程式碼運作原理 ```c uint32_t average(uint32_t a, uint32_t b) { return (a >> 1) + (b >> 1) + (EXP1); } ``` 觀察上述程式碼, ` a >> 1 ` 和 ` b >> 1` 可視為除以 2 的運算, 但當 a 以及 b 為奇數時回傳值會與正確結果差 1 因此 EXP1 需要在 a 和 b 都是奇數的情況下為 1, 其餘情況為 0 在二進位下判斷奇偶的方法可以透過確認 LSB 來做判斷, 若 LSB = 1 為奇數, LSB = 0 為偶數 所以可以將 a 和 b 與 1 做 bitwise and 來確認 LSB 的結果 * `EXP1 = a & b & 1` ```c uint32_t average(uint32_t a, uint32_t b) { return (EXP2) + ((EXP3) >> 1); } ``` 從加法器的角度切入 sum 和 carry 分別能以下列操作表示: * sum: `x ^ y` * carry: `(x & y) << 1` 把上面兩者的結果相加等同於 (x + y), 回到題目部份剛好也是兩個運算式相加, 但其中有一個 right shift, 可判斷為 (sum + carry) >> 1 後變成的結果 嘗試將 sum 和 carry 相加後做 right shift * `(((x ^ y) + (x & y) << 1)) >> 1` `= (((x ^ y) >> 1) + (x & y))` `= (x & y) + ((x ^ y) >> 1)` 最後便可推得下列結果 * `EXP2 = (x & y), EXP3 = (x ^ y)` ### 觀察編譯器行為 ```shell 0000000000000000 <average>: 0: f3 0f 1e fa endbr64 4: 55 push %rbp 5: 48 89 e5 mov %rsp,%rbp 8: 89 7d fc mov %edi,-0x4(%rbp) b: 89 75 f8 mov %esi,-0x8(%rbp) e: 8b 45 fc mov -0x4(%rbp),%eax 11: 23 45 f8 and -0x8(%rbp),%eax 14: 89 c2 mov %eax,%edx 16: 8b 45 fc mov -0x4(%rbp),%eax 19: 33 45 f8 xor -0x8(%rbp),%eax 1c: d1 e8 shr %eax 1e: 01 d0 add %edx,%eax 20: 5d pop %rbp 21: c3 retq ``` * 上面是 -O0 產生的組合語言程式碼 * 組合語言指令解析: * `push src:` 將 src 的資料 push 進 stack * `mov dest src:` 將 src 的資料搬到 dest * `xor dest src:` 將 dest 與 src 做 XOR 後存回 dest * `add dest src:` 將 dest 與 src 做 ADD 後存回 dest * `shr:` shift right * `pop dest:` 將 stack 的資料 pop 給 dest ```shell 0000000000000000 <average>: 0: f3 0f 1e fa endbr64 4: 89 f8 mov %edi,%eax 6: 31 f0 xor %esi,%eax 8: d1 e8 shr %eax a: 21 f7 and %esi,%edi c: 01 f8 add %edi,%eax e: c3 retq ``` * 上面是 -O1 產生的組合語言程式碼 * 與 -O0 最大的差別是少了 stack 相關的操作 ```shell 0000000000000000 <average>: 0: f3 0f 1e fa endbr64 4: 89 f8 mov %edi,%eax 6: 21 f7 and %esi,%edi 8: 31 f0 xor %esi,%eax a: d1 e8 shr %eax c: 01 f8 add %edi,%eax e: c3 retq ``` * 上面是 -O2 和 -O3 產生的組合語言程式碼 * 與 -O1 的差別運算順序有些許不同 ## 測驗2 ### 程式碼運作原理 ```c #include <stdint.h> uint32_t max(uint32_t a, uint32_t b) { return a ^ ((EXP4) & -(EXP5)); } ``` * 解題靈感來自於 [LeetCode 136. single number](https://leetcode.com/problems/single-number/), 透過 XOR 能夠找出 single number 的特性來做思考 * 由於程式碼左半邊的 `a ^` 的部份以固定, 因此想令括號內的結果等價為下列情形: * `a ^ b`: 當 b 比較大的時候, `a ^ a ^ b` 能夠將 a 消掉, 正好將 b 作為回傳值 * `a ^ a`: 當 a 比較大的時候, `a ^ a ^ a` 由於 a ^ a 的結果是 0, a ^ 0 後又會恢復為 a, 將 a 回傳正好也是最大值 1. 首先根據提示 EXP5 為一個 logical 的運算, 根據 C 語言規格, 邏輯運算結果只會是 0 或 1, 在灌上一個負號後的最後能產生的結果只會是 `0xffffffff` 或 `0x00000000` 1. 再來考慮 & 的特性 * `x & 1 = x`: 不變 * `x & 0 = 0`: 強制歸零 可以根據上述兩種可能的結果繼續推測 EXP4 的可能 1. 回到第一點的討論, EXP4 根據提示為 bitwise 的操作, 而我們希望能夠產出一個 XOR 的結果, 因此這邊填入 `a ^ b` 1. 由於 EXP4 也已經做選擇, 根據 & 運算性質的討論可得最後結論: * `a ^ ((a ^ b) & -(1))`: 由於 `a ^ b` 不變最後結果剩下 b * `a ^ ((a ^ b) & -(0))`: 由於括號內被歸零最後結果剩下 a 也就是當實際狀況 a > b 時, 我們希望 EXP5 的結果為 false, 最後結果才能將 a 留下, 因此 EXP5 可填入 `a < b` 同樣的驗證實際情況為 a < b 時, 因 EXP5 的結果為 true, 括號內不變而兩個 a 抵銷後最後留下了 b 整理上述的原理後可得下列結果 * `EXP4 = a ^ b`, `EXP5 = a > b` ## 測驗3 ### 程式碼運作原理 ```c= #include <stdint.h> uint64_t gcd64(uint64_t u, uint64_t v) { if (!u || !v) return u | v; int shift; for (shift = 0; !((u | v) & 1); shift++) { u /= 2, v /= 2; } while (!(u & 1)) u /= 2; do { while (!(v & 1)) v /= 2; if (u < v) { v -= u; } else { uint64_t t = u - v; u = v; v = t; } } while (COND); return RET; } ``` 以上的程式碼其實就是老師測驗題上提供的演算法的實作 * 程式的最一開始先判斷特例情況, 若有任一數字為 0 則可以直接得到 GCD * 當兩個數字都為偶數的情況下, $GCD(u, v)$ 可化簡為 $2 * GCD( \frac{u}{2} , \frac{v}{2})$, 第 6 行的迴圈便是為了簡化而做紀錄, u 和 v 每除了一次 2, 最後就要乘回來一次, 把次數紀錄在變數 shift * 根據演算法提示, 只要 u 和 v 為一奇一偶的情況則可以進一步化簡 * if u is odd and v is even, then $GCD(u, v) = GCD(u, \frac{v}{2})$ * else if u is even and v is odd, then $GCD(u, v) = GCD(\frac{u}{2}, v)$ * 第 9 和 12 行的 while 是在找尋進一步化簡的可能 * 14 至 19 行則是在實現減法版本的 Euclidean algorithm, 並在每次 do-wile 的開頭嘗試化簡的可能 * if u < v, then $GCD(u, v) = GCD(u, v - u)$ * else $GCD(u, v) = GCD(v, u - v)$ * `COND = u != 1 && v` 根據 Euclidean algorithm 的終止條件而設立 * 其中 `u != 1` 是因為當 `u == 1` 時 u 的值就不會在改變了 * `RET = u << shift` 根據前面推導若在 for 迴圈的階段有做化簡則最後要乘回來 ## 測驗4 ### 程式碼運作原理 ```c= #include <stddef.h> size_t naive(uint64_t *bitmap, size_t bitmapsize, uint32_t *out) { size_t pos = 0; for (size_t k = 0; k < bitmapsize; ++k) { uint64_t bitset = bitmap[k]; size_t p = k * 64; for (int i = 0; i < 64; i++) { if ((bitset >> i) & 0x1) out[pos++] = p + i; } } return pos; } ``` 上面的程式碼能夠找出在二進位表示中 bitset 的 bit 為 1 的位置, 並且將其紀錄在 out 裡面, 最後能夠根據回傳的 pos 在 out 中取出有效的紀錄 ```c size_t improved(uint64_t *bitmap, size_t bitmapsize, uint32_t *out) { size_t pos = 0; uint64_t bitset; for (size_t k = 0; k < bitmapsize; ++k) { bitset = bitmap[k]; while (bitset != 0) { uint64_t t = EXP6; int r = __builtin_ctzll(bitset); out[pos++] = k * 64 + r; bitset ^= t; } } return pos; } ``` * 使用 __builtin_ctzll 改寫後的結果如上, 只要當前的 bitset 不為 0, 就透過 __builtin_ctzll 從右往左找出第一個 bit 為 1 的位置 * 透過 left shift 將 1 移動到 __builtin_ctzll 回傳的位置, 最後在根據 XOR 的特性將對應位置的 bit 化為0, 而其他 bit 不變, 直到 bitset 歸零為止 綜合上述整理可得結果 * `EXP6 = 0b1L << __builtin_ctzll(bitset)` ## 測驗5 ### 程式碼運作原理 ```c= struct rem_node { int key; int index; struct list_head link; }; static int find(struct list_head *heads, int size, int key) { struct rem_node *node; int hash = key % size; list_for_each_entry (node, &heads[hash], link) { if (key == node->key) return node->index; } return -1; } char *fractionToDecimal(int numerator, int denominator) { int size = 1024; char *result = malloc(size); char *p = result; if (denominator == 0) { result[0] = '\0'; return result; } if (numerator == 0) { result[0] = '0'; result[1] = '\0'; return result; } long long n = numerator; long long d = denominator; if (n < 0) n = -n; if (d < 0) d = -d; bool sign = (float) numerator / denominator >= 0; if (!sign) *p++ = '-'; long long remainder = n % d; long long division = n / d; sprintf(p, "%ld", division > 0 ? (long) division : (long) -division); if (remainder == 0) return result; p = result + strlen(result); *p++ = '.'; char *decimal = malloc(size); memset(decimal, 0, size); char *q = decimal; size = 1333; struct list_head *heads = malloc(size * sizeof(*heads)); for (int i = 0; i < size; i++) INIT_LIST_HEAD(&heads[i]); for (int i = 0; remainder; i++) { int pos = find(heads, size, remainder); if (pos >= 0) { while (PPP > 0) *p++ = *decimal++; *p++ = '('; while (*decimal != '\0') *p++ = *decimal++; *p++ = ')'; *p = '\0'; return result; } struct rem_node *node = malloc(sizeof(*node)); node->key = remainder; node->index = i; MMM(&node->link, &heads[EEE]); *q++ = (remainder * 10) / d + '0'; remainder = (remainder * 10) % d; } strcpy(p, decimal); return result; } ``` * 程式的計算方法是模擬手算長除法時的情況 * 其中 24 ~ 33 行的部份為處理除數或被除數為零的特例, 可以直接得到結果 * 43 ~ 44 行透過將兩數相除判斷是否需要在最後結果加上負號 * 47 ~ 52 行進行了第一輪計算, 並透過 sprintf 將結果 int 的資料轉為 string 放入 result 中, 同時檢查 remainder 是否為 0, 若可以整除的話可以直接得出結果 * 54 ~ 55行透過 result 的整數部份找出放置 `'.'` 的位置後將其置入 * 57 ~ 64 行在為了後續計算建立一個 hash table, 其中 size 為 bucket 的數量 * 66行開始為長除法的計算, 長除法中判斷循環小數的方式為出現了重複的餘數 每次 for 迴圈的開始都會透過 hash 回傳的 pos 確認是否餘數已經出現過了 * 當 `pos >= 0` : 代表已能確認循環的部份, 透過回傳的 pos 先將 decimal 不循環的部份複製到 p, 之後擺上 `'('` , 把 decimal 剩下的部份還有 `')'` 跟 null terminator 放入後回傳結果 * 當 `pos == -1` : 代表尚未發現循環的部份, 因此將該輪的結果放入 hash list 中 * 84 ~ 85 行為長除法每次都需要將數字最後補零的操作 * 88 ~ 89 若能成功將餘數清為零, 則將 decimal 整個複製給 p, 最後將結果回傳 綜合上述整理可得結果 * `PPP = pos--` * PPP 會影響擺放 `'('` 的位置, 這取決於回傳的 pos, 而 pos 又紀錄了該個 remainder 是第幾 `n` 計算的結果, 因此需要在小數後 `n` 位補上 `'('` * `MMM = list_add, EEE = size % remainder` * MMM 和 EEE 為在 hash list 中新增資料的方式, 由於 hash function 為計算傳入參數 `key % size` 的結果, 而在程式碼 67 行傳入的各是 size 和 remainder, 因此加入時同樣加入在 `size % remainder` 個 bucket, 另外由於是 circular doubly linked list, 因此加在尾巴或頭都可以 ### 可改進之處 TODO ## 測驗6 ### 程式碼運作原理 ```c /* * ALIGNOF - get the alignment of a type * @t: the type to test * * This returns a safe alignment for the given type. */ #define ALIGNOF(t) \ ((char *)(&((struct { char c; t _h; } *)0)->M) - (char *)X) ``` * `__alignof__` 計算的是若想對齊某資料需要花的 byte 數量 * 根據 macro 定義傳入的參數 `t` 以及 `struct` 內會將 `t` 代換掉並同時以 `t` 的資料型態宣告變數 `_h` * 同時使用一個 literal `0` 來當載體並且計算 `_h` 與這個 `struct` 自身的差距有多少作為對齊所需的 byte 數 根據上述整理可得結果 * `M = _h` * 因為想計算的是 `t->_h` 與 `t` 的差距, 因此填入 `_h` * `X = 0` * 由於前面是用 `0` 來當載體, 後面也要一致 ## 測驗7 ### 程式碼運作原理 ```c= static inline bool is_divisible(uint32_t n, uint64_t M) { return n * M <= M - 1; } static uint64_t M3 = UINT64_C(0xFFFFFFFFFFFFFFFF) / 3 + 1; static uint64_t M5 = UINT64_C(0xFFFFFFFFFFFFFFFF) / 5 + 1; int main(int argc, char **argv) { for (size_t i = 1; i <= 100; i++) { uint8_t div3 = is_divisible(i, M3); uint8_t div5 = is_divisible(i, M5); unsigned int length = (2 << KK1) << KK2; char fmt[9]; strncpy(fmt, &"FizzBuzz%u"[(8 >> div5) >> (KK3)], length); fmt[length] = '\0'; printf(fmt, i); printf("\n"); } return 0; } ``` 首先看過整個主程式的執行流程 * 12 ~ 13 行的 is_divisible 用來測試是 i 否為 3 或 5 的倍數, 之後根據測試結果來設定 length * is_divisible 回傳的值為 0 或 1, 因此 14 行的操作也能看為根據測試結果將 length 乘以 2 * 17 行的部份透過 div5 和 div3 的結果取出 `"FizzBuzz%u"` 中的一部分並將複製到 fmt 中 * 剩下的部份為加入 NULL terminator 以及將其印出 可以發現關鍵在於: * is_divisible 的實作 * 如何透過 length, div3, div5 取出 `"FizzBuzz%u"` 的片段 #### is_divisible 在閱讀了 [Faster remainders when the divisor is a constant: beating compilers and libdivide](https://lemire.me/blog/2019/02/08/faster-remainders-when-the-divisor-is-a-constant-beating-compilers-and-libdivide/) 網頁中提出的方法後得知若想計算除法可以透過乘法的模擬得出 * $\Large \frac{n}{d} = n * \frac{2^N}{d} * \frac{1}{2^N}$ * 其中 $\Large \frac{2^N}{d}$ 是需要預計算的部份, $\Large \frac{1}{2^N}$ 可以透過 shift 得出, 因此往後我們便能只透過乘法來得出除法的結果 * 餘數的部份可從上述公式中的 LSB 乘上 d 後做 right shift 獲得 #### 控制 `"FizzBuzz%u"` 的片段 由於 `"FizzBuzz%u"` 會需要印出的片段分別為: * Fizz: 位移量 0, 長度 4 * Buzz: 位移量 4, 長度 4 * `%u`: 位移量 8, 長度 2 * FizzBuzz: 位移量0, 長度8 需要控制的變數皆為 powers of 2, 因此可以用 8 為底搭配 right shift 的操作來取出所需片段 綜合上述討論可得: * `KK1 = div3, KK2 = div5` * length 是以 2 為底來做 shift 的, 而當 div3 或 div5 的值為 1 時便可以透過 left shift 變為 4 或 8 * `KK3 = div3 << 2` * 題目已知的部份有 `(8 >> div5) >>` 先討論 div5 為 1 的情況, 結果會變成 `4 >> (KK3)`, 而這時 div3 還沒有用上, 且希望透過 div3 來控制要印出 `FizzBuzz` 或是 `Buzz` * 這兩者的位移量分別是 0 以及 4, 所以 `KK3` 需要在真的時候產出 2 ,假的時候產出 0 的結果, 以便讓`(8 >> div5) >> (KK3)`能夠符合預期行為 * 因此 KK3 填入 `div3 << 2`