# 2022q1 Homework2 (quiz2) contributed by <`scottxxxabc`> ## 測驗 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); } ``` --- ### `EXP1` : $low + (high - low) / 2$ 即為 $low / 2 + high / 2$,由題目可觀察到當 `a` 、`b` 皆為偶數時 `a >> 1` 及 `b >> 1` 分別等價於 $low /2$ 、 $high / 2$ 。 但當 `a` 、`b` 皆為奇數時,答案會和預期的少 1。因此 `EXP1` 為 `a & b & 1`。 --- - [ ] 我們再次改寫為以下等價的實作: ```c uint32_t average(uint32_t a, uint32_t b) { return (EXP2) + ((EXP3) >> 1); } ``` --- ### `EXP2` 、 `EXP3` : 此題須以半加器思考。如下表,`x`、`y`相加可視為 `x ^ y`,而 `carry` 則等於 `x & y` | x | y | x+y |carry| | -------- | -------- | -------- | -------- | | 0 | 0 | 0 |0| | 0 | 1 | 1 |0| | 1 | 0 | 1 |0| | 1 | 1 | 0 |1| 將半加器拓展至 `uint32_t` ,可寫為 `x ^ y + (x & y) << 1`。又題目要求為 $(a+b)/2$,因此應回傳 `(x ^ y) >> 1 + x & y`。 ## 測驗 2 - [ ] 改寫〈解讀計算機編碼〉一文的「不需要分支的設計」一節提供的程式碼 min,我們得到以下實作 (max): ```c #include <stdint.h> uint32_t max(uint32_t a, uint32_t b) { return a ^ ((EXP4) & -(EXP5)); } ``` 其中 EXP4 為 a 和 b 進行某種特別 bitwise 操作,限定用 OR, AND, XOR 這三個運算子。EXP5 為 a 和 b 的比較操作,亦即 logical operator,限定用 >, <, ==, >=, <=, != 這 6 個運算子。 --- 由題目可知 `EXP5` 的值應為 0 或 1。`a ^ ((EXP4) & -(EXP5))` 表示式在 `EXP5` 的值為 0 時會回傳 `a ^ 0`,根據 XOR 特性,`a ^ 0` 等於 `a`。因此 `EXP5` 應為 `a < b`。 當 `a > b` 時,`a ^ ((EXP4) & -(EXP5))` 等於 `a ^ (EXP4) ` 。此時應回傳 `b`,因此 `EXP4` 為 `a ^ b`。 ## 測驗 3 考慮以下 64 位元 GCD (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; } ``` --- 此為輾轉相除法的實作,終止條件為 `v` 為零,因此 `COND` 為 `v`。 由題目之敘述 > If x and y are both even, $gcd(x, y) = 2 * gcd(x/2, y/2)$ 可觀察到 `for` 迴圈為上述算法的實作,因此 `RET` 為 `u << shift`。 ## 測驗4 在影像處理中,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 的行為是回傳由低位往高位遇上連續多少個 0 才碰到 1。 用以改寫的程式碼如下: ```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 的效益就更高。 --- 此題須找出最低位元的 1,根據題目的提示利用 2 補數的特性 `-bitset = ~bitset + 1`,可以觀察到若 `bitmap` 為 `...XXX1000...`,則`~bitmap` 為 `...~X~X~X0111...`,`~bitmap + 1` 為 `...~X~X~X1000...`,利用 `&` 可以將其他的位元設為 0。 ## 測驗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; } ``` --- 首先觀察 `find` 函示,不難發現 `find` 的作用是在 Hash table 中尋找指定的 `key` 並返回 `index`,hash function 為 `key % size`。觀察`fractionToDecimal`函式,發現在 41 ~ 63 行的作用是把整數的部分存入 `result` 中。 72 行開始將 Hash table 初始化,由 72 行可知hash function 的 `size` 為 1333 。 78 行將 `pos` 設為 `find` 回傳的 `index` ,第 91 行可發現 `index` 為 `i`,可推測 93 行應該將 `node` 加入 Hash table ,因此 `MMM` 為 list_add , `EEE` 為 `&heads[remainder % size]` ,其中 array index 即為 hash function。 由 96 行發現每次迴圈 `remainder` 會乘 10 ,因此 `i` 值為小數後的位數。觀察 `PPP` 所在的 `while` 迴圈,可發現迴圈應該執行的次數等於小數點後為循環的位數,也就是找到相同的 hash 之前。因此 `PPP` 為 `pos--` 。 ## 測驗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) ``` [你所不知道的 C 語言:記憶體管理、對齊及硬體特性](https://hackmd.io/@sysprog/c-memory?type=view) 提到 > struct 會自動做 alignment,假設創了一個 struct,如下面 code 所示 ``` struct s1 { char c; int a; } ``` > 原本推論 char 為 1 byte,而 int 為 4 byte ,兩個加起來應該為 5 byte,然而實際上為 8 byte,由於 int 為 4 byte ,所以 char 為了要能 alignment 4 byte 就多加了 3 byte 進去 ,使得 cpu 存取速度不會因 address 不是在 4 的倍數上,存取變慢。 編譯器會在根據下一個變數的大小在 `char c` 後面加入適當的 padding。如上述的變數 int 就會加入 3 byte padding。 > [Wiki](https://en.wikipedia.org/wiki/Data_structure_alignment) > ... ,and GNU (GCC) when compiling for 32-bit x86: > - A char (one byte) will be 1-byte aligned. > - A short (two bytes) will be 2-byte aligned. > - An int (four bytes) will be 4-byte aligned. > - A long (four bytes) will be 4-byte aligned. > - A float (four bytes) will be 4-byte aligned. > - A double (eight bytes) will be 8-byte aligned on Windows and 4-byte aligned on Linux (8-byte with -malign-double compile time option). > - A long long (eight bytes) will be 4-byte aligned. > - A long double (ten bytes with C++ Builder and DMC, eight bytes with Visual C++, twelve bytes with GCC) will be 8-byte aligned with C++ Builder, 2-byte aligned with DMC, 8-byte aligned with Visual C++, and 4-byte aligned with GCC. > - Any pointer (four bytes) will be 4-byte aligned. (e.g.: char*, int*) 由此可推得 `struct { char c; t _h; }` 中的 `c` 會根據 `_h`的型態自動做適當的 alignment。 題目中將 `0` cast 成 `struct { char c; t _h; }` 型態的指標,題目所求的 alignment 為 `c` 及 padding 的大小,即為 `_h` 的位址減去 `c` 的位址。因此題目的 `&((struct { char c; t _h; } *)0)->M)` 中的 `M` 應為 `_h` 。 `(char *)X` 為 `c` 的位址,也就是 struct 的起始位址,先前將 `0` cast 成 `struct` 型態的指標,所求即為 `0` 的位址,因此 `X` 為 `0` 。 ## 測驗 7 考慮貌似簡單卻蘊含實作深度的 FizzBuzz 題目: - 從 1 數到 n,如果是 3的倍數,印出 “Fizz” - 如果是 5 的倍數,印出 “Buzz” - 如果是 15 的倍數,印出 “FizzBuzz” - 如果都不是,就印出數字本身 ```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"[(9 >> div5) >> (KK3)], length); fmt[length] = '\0'; printf(fmt, i); printf("\n"); } return 0; } ``` 觀察 `is_divisible` 可發現函式回傳 `n` 是否能整除 `M`,可以整除回傳 1,不能回傳 0。 第 14 行設定字串的長度,只能整除 3 或 5,length 為 4 ; 若整除 15,length 為 8 ; 不能整除 3 或 5,length 為 2。因此 `KK1` 為 `div3` , `KK5` 為 `div5` 第 17 行設定字串的起始位置,整除 3 或 15 起始位置為 0 ; 若整除 5, 起始位置為 4 ; 不能整除 3 或 5,起始位置為 8。將 9 右移到 0 至少要右移 4 位元,因此 `KK3` 為 `div3 << 2`