# 2022q1 Homework2 (quiz2) contributed by <`arthurchang09`> ## 測驗 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); } ``` `a>>1` 和 `b>>1` 可視為 `a/2` 、 `b/2` 。然而,若 a 和 b 皆為奇數的話,顯然會和所要的結果差 1 。因此 `EXP` 需要確認 `a` 和 `b` 的最後的 bit 為 1 並加上,最直觀的方法為和 1 取 and ,若為偶數則會變成 0 ,奇數為 1。 `EXP` 為 `a & b & 1` 。 我們再次改寫為以下等價的實作: ```c uint32_t average(uint32_t a, uint32_t b) { return (EXP2) + ((EXP3) >> 1); } ``` 這邊可以運用半加器的方式思考。半加器是由一個 `AND` 和 `XOR` 組成。其真值表如下: | A | B |A AND B|A XOR B| |:-:|:-:|:-:|:-:| |0|0|0|0| |0|1|0|1| |1|0|0|1| |1|1|1|0| 因此 A XOR B 是兩數相加此位數的值,而 A AND B 是進位。考慮所求為 $\dfrac{A+B}{2}$ , XOR 的部分需右移一位,即除以2方能表示此位數的狀況。進位的部分原本應左移一位加到下一位數,因為除以二而變成沒有左移。因此 EXP2 為 `a & b` 而 EXP3 為 `a ^ b >> 1` ## 測驗二 ```c #include <stdint.h> uint32_t max(uint32_t a, uint32_t b) { return a ^ ((EXP4) & -(EXP5)); } ``` 此題會運用 XOR 的幾個特性 * a ^ 0 = a * a ^ (a ^ b) = b 因此,必須設法使 `((EXP4) & -(EXP5))` 中最後的值為 0 或 a ^ b 。若 a 大於 b 則為 0 , a 小於 b 則為 a ^ b 。此時可以看到 & 在 EXP4 和 EXP5 之間且 EXP5 是帶有大於小於符號,可判斷 EXP4 為 `a ^ b` 而 EXP5 透過 `&` 使 `((EXP4) & -(EXP5))` 為 a ^ b 或 0 。 當 `EXP5` 為 0 ,`-(EXP5)` 為 0 ,經過 `&` 使 `((EXP4) & -(EXP5))` 為 0 , a ^ 0 為 a 。 當 `EXP5` 為 1 , `-(EXP5)` 為 `0xffffffff` ,經過 `&` 使 `((EXP4) & -(EXP5))` 為 `EXP4` ,即 `a ^ b` 。 又此函式求兩數最大值,因此 `EXP5` 為 `a < b` 。 ## 測驗三 ```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; } ``` 上方程式碼為 [Binary GCD algorithm](https://en.wikipedia.org/wiki/Binary_GCD_algorithm),演算法如下 1. gcd(0, v) = v, because everything divides zero, and v is the largest number that divides v. Similarly, gcd(u, 0) = u. 2. gcd(2u, 2v) = 2·gcd(u, v) 3. gcd(2u, v) = gcd(u, v), if v is odd (2 is not a common divisor). Similarly, gcd(u, 2v) = gcd(u, v) if u is odd. 4. gcd(u, v) = gcd(|u − v|, min(u, v)), if u and v are both odd. 首先,若兩數有一者為 0 ,則回傳另一數。這邊的回傳使用 `u | v` ,當其中一者為 0 時, or 的結果為另一數,同上述第 1 點。 若兩數皆為偶數,即 LSB 為 0 ,即可先除以二,這裡使用 for 迴圈將兩數同時除以二,直到有一數為奇數,並用 `shift` 記下除了幾次。 由於 2 不再是公因數,使用第一個 `while` 將其中一數 u 的 2 全部除乾淨。 接著使用 `do_while` 迴圈尋找奇數之公因數。因為 `v` 可能為偶數,且目前 `u` 和 `v` 之公因數不會有 2 ,故先除掉,如同地 3 點。接著進行減法找公因數,若 `v` 較大則 `v - u` 為正,直接進行下一次迴圈,若 `v` 較小,則將兩者之差 assign 給 `v` , 將原本 `v` 之值給 `u` ,如同第四點所述。在此迴圈中主要被減的數為 `v` ,當 `v = 0` 時, `u` 之值為最大公因數中的奇數部分。接著,根據上述第 2 點要將兩數字被同除的 2 乘回。因此 COND 為 `v` , RET 為 `u<<shift` 。 ## 測驗四 ```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 為 $000101_b$,那 t 就會變成 $000001_b$,接著就可以透過 XOR 把該位的 1 清掉,其他保留 (此為 XOR 的特性)。 考慮到二補數之特性,某數先經過 `not` ,即 1 變 0 或 0 變 1 ,接著 `+ 1` 的話,若最低位元為 0 ,`not` 後為 1 ,相加進位為 0 ,直到某數某位元為 1 , `not` 後為 0 ,相加結果為 1 ,進位即停止,某數最低位元的 1 的位置會變成 1 。此時較高位元和原數同樣位置呈 `not` 關係,較低位元皆為 0 。 如 `a = 10110` , `~a = 01001` , `~a + 1 = 01010` 。接著將某數與其二補數做 & ,即完成所求。 `a & -a = 10110 & 01010 = 00010` 。 ## 測驗五 ```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; } ``` :::info 尚須修改 ::: 程式一開始為輸出的字串配置空間,大小為 1024 byte ,接著算商和餘數,並使用 `sprintf` 將商放入 `result` 字串。接著在商後放入小數點。由於題目需要找到循環小數,這裡採用 hash table 存放每一位小數點後的數字。因此第 72 行到 第 75 行初始化 hash table。 第 77 行 `for` 迴圈是要尋找循環小數, `i` 表示目前處理到第幾位小數。一開始會先去 hash table 尋找數字是否有重複出現,若有的話,先將此位數之前的數字放入 `result` 中,在第 81 行, `*p` 是目前要放入值的位址,而 `deciaml` 則存著目前已處理的小數, `pos` 則是 hash table 中存放相同數值隻小數的位子,因此 PPP 應為 `pos--` ,才能完成前述目的。 接著,放入 `(` 再用 `while` 迴圈將重複數字後的位數一一放入 `result` 中,最後放入 `)` 和 `\0` 表示循環小數結束及字串結束,並回傳 `result` 。 若第 78 行沒有找到位置,表示數字沒重複,要在 hash table 中加入新的節點,因此第 89 到第 93 行即是初始化節點並插入。 MMM 為 `list_add` 將節點插入對應之 linked list 開頭,而所插入的 linked list 需透過 hash 找到是在 `heads` 陣列中第幾個元素,因此 EEE 為 `&heads[remainder % size]` 。接著將目前處理的小數放入 `decimal` 中並更新 `remainder` 。 最後若沒有循環小數,則將 `decimal` 複製到 `result` 小數點後,並回傳結果。 ## 測驗六 `__alignof__` 是 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; } *)0` ,這裡宣告一個 `struct` 並將 0 轉型為 `struct *` ,使此 `struct` 的位址起點在 0 。在 `struct` 中,每一個成員所占用的記憶體長度不同,電腦為了方便記憶體存取,會額外給空間使整著 `struct` 對齊。舉個例子如下: ```c struct example{ char c; // 1 byte int i; // 4byte }; ``` 為了對齊,實際上 `c` 後面會額外接上 3 byte ,使其長度和 `i` 一致。因此 `c` 的位址和 `i` 的位址之間是差 4 byte 而非 1 byte。 回到題目,由於以上的特性,想要求出 alignment ,必定要找到所傳入之 `t` 在這個 `struct` 的位址,因此 M 為 `_h` 。由於已將整個 `struct` 之位址定在 0 , 要求出 alignment 則不必再寫出這一長串 `&((struct { char c; t _h; } *)0)->c` , `(char *)0` ,即可。因此 X 為 0 。 ## 測驗七 考慮貌似簡單卻蘊含實作深度的 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; } ``` `for` 迴圈中, `div3` 和 `div5` 為某數能否被 3 或 5 整除,為真則為 1 ,反之為 0 。接著要透過控制 length 來決定輸出之方式,如題目所示: ```c string literal: "fizzbuzz%u" offset: 0 4 8 ``` 因此當某數為 3 倍數,需要印出 fizz , `offset = 4 = 2 << 1` , 因此 KK1 為 `div3` ,若同時為 5 之倍數,要再印出 buzz , `offset = 8 = (2 << 1) << 1` ,因此 KK2 為 `div5` 。 接下來第 17 行便是決定要從 `"fizzbuzz%u"` 的哪個位置開始印出字串,若 `div3` 和 `div5` 皆為 0 ,則要從第 9 個字元,即數字部分開始印出。若只是為 3 倍數,要從最開頭開始印, 9 須被位移成 0 , `9 >> 4 = 9 >> (1 << 2)` ,因此 KK3 為 2 。若只是為 5 倍數,要從第 4 個元素開始印 , 9 須被位移成 4 , `9 >> 1 = 1001 >> 1 = 0100 = 4` 。 若為 15 及其倍數,則一樣須從開頭開始印, 9 被位移成 0 , `(9 >> 1) >> (1 << 2) = 4 >> 4 = 0` 。最後再印出處理好的字串。 :::info 測試的時候發現 "fizzbuzz%u" 應該要改為 "fizzbuzz %u" ,否則不會印出數字而是 u 。 :::