# 2022q1 Homework2 (quiz2) contributed by < `rickywu0421` > [題目連結](https://hackmd.io/@sysprog/linux2022-quiz2) ## 測驗 `1` ### 題目 考慮以下對二個無號整數取平均值的程式碼: ```c #include <stdint.h> uint32_t average(uint32_t a, uint32_t b) { return (a + b) / 2; } ``` 這個直覺的解法會有 overflow 的問題,若我們已知 a, b 數值的大小,可用下方程式避免 overflow: ```c #include <stdint.h> uint32_t average(uint32_t low, uint32_t high) { return low + (high - low) / 2; } ``` 接著我們可改寫為以下等價的實作: ```c #include <stdint.h> uint32_t average(uint32_t a, uint32_t b) { return (a >> 1) + (b >> 1) + (EXP1); } ``` 我們再次改寫為以下等價的實作: ```c uint32_t average(uint32_t a, uint32_t b) { return (EXP2) + ((EXP3) >> 1); } ``` ### 解題 第一種實作方式為將 `a` 與 `b` 分別 right shift 1 bit 後相加, 此時還考慮到進位: `a` 與 `b` 的 LSB 是否都為 1, 故 EXP1 = `a & b & 1`。 第二種實作方式為加法器的概念, `x ^ y` 表示為位元和, `x & y` 表示為進位, 則 `a + b = x ^ y + (x & y) << 1` => `(a + b) / 2 = ((x ^ y) + (x & y) << 1) >> 1 = (x & y) + (x ^ y) >> 1` 故 EXP2 = `x & y`, EXP3 = `x ^ y` ## 測驗 `2` ### 題目 改寫〈[解讀計算機編碼](https://hackmd.io/@sysprog/binary-representation)〉一文的「不需要分支的設計」一節提供的程式碼 min,我們得到以下實作 (max): ```c #include <stdint.h> uint32_t max(uint32_t a, uint32_t b) { return a ^ ((EXP4) & -(EXP5)); } ``` ### 解題 我們可以透過回傳值猜測題目中的表示式: - 當 `a > b` 時, 回傳 `a`; - 當 `b > a` 時, 回傳 `b`; 又 XOR 有兩個特性 (x 為任一整數): - `x ^ x = 0` - `0 ^ x = x` 故我們可以使用這兩個特性猜測題目的解, - 當 `a > b` 時, 回傳 `a ^ 0 = a` - 當 `a < b` 時, 回傳 `a ^ a ^ b = b` 因此 EXP4 = `a ^ b`。 再根據上面的判斷, `a > b` 時 `EXP4 & -(EXP5) = 0`, `a < b` 時 `EXP4 & -(EXP5) = a ^ b` 可知, EXP5 在 `a > b` 時為 `0`, 在 `a < b` 時為 `-1` (全部位元為 1), 故可知 EXP5 = `a < b`。 ## 測驗 `3` ### 題目 考慮以下 64 位元 GCD ([greatest common divisor](https://en.wikipedia.org/wiki/Greatest_common_divisor), 最大公因數) 求值函式: ```c #include <stdint.h> uint64_t gcd64(uint64_t u, uint64_t v) { if (!u || !v) return u | v; while (v) { uint64_t t = v; v = u % v; u = t; } return u; } ``` 改寫為以下等價實作: ```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; } ``` ### 解題 第二種實作與第一種實作均是輾轉相除法, 但作法略為不同, 第一種利用除法運算, 而第二種利用減法運算, 以下是運算時會用到的操作, 以及對應的程式碼片段: 1. $gcd(0, 0) = 0$ 2. $gcd(u, 0) = u$, $gcd(0, v) = v$ ```c if (!u || !v) return u | v; ``` --- 3. 若 $u$ 與 $v$ 均是偶數, 則 $gcd(u, v) = 2 * gcd(\dfrac{u}{2}, \dfrac{v}{2})$ ```c for (shift = 0; !((u | v) & 1); shift++) { u /= 2, v /= 2; } ``` --- 4. 若 $u$ 為偶數, $v$ 為奇數, 則 $gcd(u, v) = gcd(\dfrac{u}{2}, v)$ ```c while (!(u & 1)) u /= 2; ``` --- 5. 若 $u$ 為奇數, $v$ 為偶數, 則 $gcd(u, v) = gcd(u, \dfrac{v}{2})$ ```c while (!(v & 1)) v /= 2; ``` 這個部分的程式碼是在 do-while loop 裡的, 因為每次 loop 完 v 都會更新而有機會變成 2 的倍數。 --- 核心部分:輾轉相除法 ```c 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; ``` loop 中首先進行上述的操作 5, 接著判斷 $u$ 與 $v$ 兩者誰大, 用大數剪去小數, $v$ 值更新為相減後的數, $u$ 值更新為 $u$, $v$ 中較小的數, 重複進行上述操作直到兩數相減為 `0`。 故 COND = `v`, return 值則需要乘上透過操作 3 除去的倍率 (`2` 的冪), 故 RET = `u << shift`。 ## 測驗 `4` ### 題目 在影像處理中,[bit array](https://en.wikipedia.org/wiki/Bit_array) (也稱 bitset) 廣泛使用,考慮以下程式碼: ```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; } ``` 考慮 GNU extension 的 [`__builtin_ctzll`](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html) 的行為是回傳由低位往高位遇上連續多少個 0 才碰到 1。 > 範例: 當 a = 16 16 這個十進位數值的二進位表示法為 00000000 00000000 00000000 00010000 從低位元 (即右側) 往高位元,我們可發現 0 → 0 → 0 → 0 → 1,於是 ctz 就為 4,表示最低位元往高位元有 4 個 0 用以改寫的程式碼如下: ```c= #include <stddef.h> 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; } ``` 其中第 9 行的作用是找出目前最低位元的 1,並紀錄到 t 變數。舉例來說,若目前 bitset 為 000101b,那 t 就會變成 000001b,接著就可以透過 XOR 把該位的 1 清掉,其他保留 (此為 XOR 的特性)。 若 bitmap 越鬆散 (即 1 越少),於是 improved 的效益就更高。 ### 解題 根據原程式碼片段: ```c size_t p = k * 64; for (int i = 0; i < 64; i++) { if ((bitset >> i) & 0x1) out[pos++] = p + i; } ``` 其意義為從 LSB, 迭代檢查 bitset 的該 bit 是否為 1, 若是, 則 `out[pos++] = k * 64 + 該 bit 的位置`。 改良的程式碼片段如下: ```c uint64_t t = EXP6; int r = __builtin_ctzll(bitset); out[pos++] = k * 64 + r; bitset ^= t; ``` 其概念為透過 `__builtin_ctzll()` 找出 bitset 中從低位元往高位元數來的第一個 1, 再透過 `bitset ^= t` 把那一個 1 clear, 以便進行下一個 set bit 的搜索。 假設 `bitset = 0001 0100`, 則做完一次上述的操作後位置 2 的 bit 會被 clear:`bitset = 0001 0000`, 而要怎麼達成這件事呢? 根據題目的提示 (-bitset), 根據二補數, `-bitset = 1110 1100`, 我們可發現, 將 `t = bitset & -bitset` 後可以得到一個 `bit string = 0000 0100`, 其只有一個 bit 被 set, 那就是 bitset 中最小位元為 1 的位置, 之後再透過 `bitset ^= t` 把 bitset 中最小位元的 1 clear 掉 (根據 xor 的特性), 故 EXP6 = `bitset & -bitset` ## 測驗 `5` ### 題目 以下是 LeetCode 166. Fraction to Recurring Decimal 的可能實作: ```c #include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include "list.h" 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; } /* using long long type make sure there has no integer overflow */ long long n = numerator; long long d = denominator; /* deal with negtive cases */ 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++ = '.'; /* Using a map to record all of reminders and their position. * if the reminder appeared before, which means the repeated loop begin, */ 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, EEE); *q++ = (remainder * 10) / d + '0'; remainder = (remainder * 10) % d; } strcpy(p, decimal); return result; } ``` ### 解題 以下我們以循環小數 $12 / 11 = 0.090909...$ 作為該題的例子。 該題記錄小數的作法為:用 hash table 記錄每次迭代的餘數及其在小數中的位置。 #### iter 1 每次迭代時首先會檢查該次的餘數是否存在於 hash table 中, ```c int pos = find(heads, size, remainder); if (pos >= 0) { ... } ``` 若是, 則此數為循環小數, 並進行以下操作: ```c while (PPP > 0) *p++ = *decimal++; *p++ = '('; while (*decimal != '\0') *p++ = *decimal++; *p++ = ')'; *p = '\0'; return result; ``` 但首次進入迴圈時因尚未有餘數加入到 hash table 中, 故 if 判斷不成立, 所以我們先往後看: --- 以下操作不難看出為將目前的餘數及餘數的位置加入到 hash table 中, 以當前的狀態, 餘數為 `12 % 11 = 1`, index 為 `i = 0`。 ```c struct rem_node *node = malloc(sizeof(*node)); node->key = remainder; node->index = i; list_add(&node->link, &heads[remainder % size]); ``` --- 加入 hash table 後將餘數 * 10 後除以除數: `*q++ = (1 * 10) / 11 + '0' = '0'` =>`result = '0'` 餘數則進行以下更新: `remainder = (1 * 10) % 11 = 10`。 ```c *q++ = (remainder * 10) / d + '0'; remainder = (remainder * 10) % d; ``` #### iter 2 此時 `remainder = 10`, 故以下條件仍然不成立。 ```c int pos = find(heads, size, remainder); ``` 之後的操作與 iter 1 類似, 接近 loop end 時 `*q++ = (10 * 10) / 11 + '0' = '9'` => `result = '09'` `remainder = (10 * 10) % 11 = 1` #### iter 3 此時 `remainder = 1`, 故 if 條件成立: 第一個 while loop 的功能為是將像 `1.34(15)` 中未循環的小數寫入 result 中, 故 PPP = `pos--`。(`12 / 11` 中不存在未循環的小數)。 之後將循環的小數用小括號包起來, 最後回傳 `result = '0.(09)'`。 ```c if (pos >= 0) { while (PPP > 0) *p++ = *decimal++; *p++ = '('; while (*decimal != '\0') *p++ = *decimal++; *p++ = ')'; *p = '\0'; return result; } ``` ## 測驗 `6` [\_\_alignof__](https://gcc.gnu.org/onlinedocs/gcc/Alignment.html) 是 GNU extension,以下是其可能的實作方式: ```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) ``` ### 解題 透過題目的 `struct { char c; t _h; }` 可以猜到 `__alignof__` 的實作想法為將欲驗證的 type 之變數宣告在 struct 中的 char 變數之後, 透過 `_h` 的位置減去 `c` 之位置可得之 type `t` 之 alignment。 故 M = `_h`, 則 `((char *)(&((struct { char c; t _h; } *)0)->_h)` 表示為變數 `_h` 的相對於 struct 起始的位置, 又因起始位置為 `0`, 則 X = `0`。 ## 測驗`7` ### 題目 考慮貌似簡單卻蘊含實作深度的 FizzBuzz 題目: - 從 1 數到 n,如果是 3的倍數,印出 “Fizz” - 如果是 5 的倍數,印出 “Buzz” - 如果是 15 的倍數,印出 “FizzBuzz” - 如果都不是,就印出數字本身 直覺的實作程式碼如下: (naive.c) ```c #include <stdio.h> int main() { for (unsigned int i = 1; i < 100; i++) { if (i % 3 == 0) printf("Fizz"); if (i % 5 == 0) printf("Buzz"); if (i % 15 == 0) printf("FizzBuzz"); if ((i % 3) && (i % 5)) printf("%u", i); printf("\n"); } return 0; } ``` 觀察 printf 的(格式)字串,可分類為以下三種: 1. 整數格式字串 "%d" : 長度為 2 B 2. “Fizz” 或 “Buzz” : 長度為 4 B 3. “FizzBuzz” : 長度為 8 B 考慮下方程式碼: ```c char fmt[M9]; strncpy(fmt, &"FizzBuzz%u"[start], length); fmt[length] = '\0'; printf(fmt, i); printf("\n"); ``` 我們若能精準從給定輸入 `i` 的規律去控制 `start` 及 `length`,即可符合 FizzBuzz 題意: ```c string literal: "fizzbuzz%u" offset: 0 4 8 ``` 以下是利用 bitwise 和上述技巧實作的 FizzBuzz 程式碼: (bitwise.c) ```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; } ``` 其中 `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/),甚至 gcc-9 還內建了 [FizzBuzz optimization](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=82853) (Bug 82853 - Optimize x % 3 == 0 without modulo)。 請補完,使得程式碼符合預期,儘量以最簡短且符合一致排版的形式來撰寫。 對於處理器來說,每個運算所花的成本是不同的,比如 add, sub 就低於 mul,而這些運算的 cost 又低於 div 。依據〈[Infographics: Operation Costs in CPU Clock Cycles](http://ithare.com/infographics-operation-costs-in-cpu-clock-cycles/)〉,可發現整數除法的成本幾乎是整數加法的 50 倍。 ![](https://i.imgur.com/I0nzfxz.png) 作答區 KK1 = ? (變數名稱) KK2 = ? (變數名稱) KK3 = ? (表示式,不包含小括號) ### 解題 首先看題目的這段敘述: ```c string literal: "fizzbuzz%u" offset: 0 4 8 ``` 根據是否能被 3 (`div3`) 或 5 (`div5`) 整除而產生的 `offset` 以及 `length` 變化可以做成下列表格, 以下欄位以 (div3, div5) 呈現: | (div3, div5) | (0, 0) | (1, 0) | (0, 1) | (1, 1) | | --- | -------- | -------- | -------- | ---| | offset | 8 | 0 | 4 | 0 | | length | 2 | 4 | 4 | 8 | 回到程式碼, 根據上述表格可以作答: - length 部分 ```c uint8_t div3 = is_divisible(i, M3); uint8_t div5 = is_divisible(i, M5); unsigned int length = (2 << KK1) << KK2; ``` KK1 = `div3`, KK2 = `div5` - offset 部分 ```c strncpy(fmt, &"FizzBuzz%u"[(8 >> div5) >> (KK3)], length); ``` KK3 = `div3 << 2`