# 2022q1 Homework2 (quiz2) contributed by < `Korin777` > ## 測驗一 ### 延伸問題一 考慮以下對二個無號整數取平均值的程式碼: ```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; } ``` * 先將大數中的 `low` 提出後 , 在對兩數差取平均 ```c #include <stdint.h> uint32_t average(uint32_t a, uint32_t b) { return (a >> 1) + (b >> 1) + (a & b & 1); } ``` * `(a >> 1) + (b >> 1)` 相當於 `2/a + 2/b` , 當兩數皆為奇數時多出來的餘數會使得 `a/2 + b/2`比`(a+b)/2`少1 * 因此我們要判斷兩數是否皆為奇數 , 透過檢查最低位元是否為1(即`&1`)來達成 ```c uint32_t average(uint32_t a, uint32_t b) { return (a & b) + ((a ^ b) >> 1); } ``` * `a & b`會找出2進位表示中 , 兩數皆為1的位元 * `a ^ b`會找出2進位表示中 , 只有一數為1的位元 * 而後者因為來源只有一數 , 所以要在除以2 ### 延伸問題二 透過 `objdump -d` 來查看對應的組合語言輸出 1. (a + b) / 2 ```c lea (%rdi,%rsi,1),%eax shr %eax retq ``` * `rdi` : 儲存函式第一個 argument ,`rsi` : 儲存函式第二個 argument * `eax` : return value ,為 `rax` 的 Low 32-bits * `(%rdi,%rsi,1)` : `rdi` 裡的值與 1 倍的 `rsi` 裡的值的和 => a + b * `lea (%rdi,%rsi,1),%eax` : 複製 `(%rdi,%rsi,1)` 的值到 `%eax` => %eax = a + b * `shr %eax` : 將 `%eax` 裡的值右移一位 => %eax = (a + b) / 2 * `retq` : 結束 callee, 回到 caller 原本執行的地方 2. low + (high - low) / 2 ```c mov %esi,%eax sub %edi,%eax shr %eax add %edi,%eax retq ``` * `edi` : 儲存函式第一個 argument ,`esi` : 儲存函式第二個 argument * `rdi` 佔 64 bits,而 `edi` 則是 `rdi` 的 Low 32-bits * `mov %esi,%eax` : 將 `%esi` 裡的值複製到 `%eax` => %eax = low * `sub %edi,%eax` : 將 `%eax` 裡的值與 `%edi` 裡的值差存在 `%eax` => %eax = low - high * `shr %eax` : %eax = (low - high) / 2 * `add %edi,%eax` : 將 `%eax` 裡的值與 `%edi` 裡的值和存在 `%eax` => %eax = low + (low - high) / 2 3. (a >> 1) + (b >> 1) + (a & b & 1) ``` mov %esi,%edx shr %esi and $0x1,%edx and %edi,%edx shr %edi add %edx,%edi lea (%rdi,%rsi,1),%eax retq ``` * `%edx` : 儲存函式第三個 argument,因為函式只有兩個參數,這裡應該是用來暫存計算中的值 * `mov %esi,%edx` : %edx = a * `shr %esi` : %esi = a >> 1 * `and $0x1,%edx` : 將常數 `0x1` 與 `%edx` 裡的值做 & 運算並存在 `%edx` => %edx = a & 1 * `and %edi,%edx` : %edx = a & 1 & b * `shr %edi` : %edi = b >> 1 * `add %edx,%edi` : %edi = (b >> 1) + (a & b & 1) * `lea (%rdi,%rsi,1),%eax` : %eax = (a >> 1) + (b >> 1) + (a & b & 1) 4. (a & b) + ((a ^ b) >> 1) ```c mov %edi,%eax and %esi,%edi xor %esi,%eax shr %eax lea (%rax,%rdi,1),%eax retq ``` * `mov %edi,%eax` : %eax = a * `and %esi,%edi` : %edi = a & b * `xor %esi,%eax` : %eax = a ^ b * `shr %eax` : %eax = (a ^ b) >> 1 * `lea (%rax,%rdi,1),%eax` : %eax = (a & b) + ((a ^ b) >> 1) 參考資料 * [Guide to x86-64](https://web.stanford.edu/class/archive/cs/cs107/cs107.1166/guide_x86-64.html) * [CS107, Lecture 11 Assembly: Arithmetic and Logic](https://web.stanford.edu/class/archive/cs/cs107/cs107.1222/lectures/11/Lecture11.pdf) ## 測驗二 ### 延伸問題一 ```c #include <stdint.h> uint32_t max(uint32_t a, uint32_t b) { return a ^ ((a ^ b) & -(a < b)); } ``` * 當 b > a 1. a ^ ((a ^ b) & 0xffff) => x & 1 = x 2. a ^ (a ^ b) => xor associative 3. (a ^ a) ^ b => x ^ x = 0 4. 0 ^ b => 0 ^ x = x 5. b * 當 a >= b 1. a ^ ((a ^ b) & 0) => x & 0 = 0 2. a ^ 0 => x ^ 0 = x 3. a ### 延伸問題二 參考上面 32 位元無號整數實作,撰寫針對 32 位元有號並同樣 branchless 的實作 ```c int32_t max(int32_t a, int32_t b) { return a ^ ((a ^ b) & -(a < b)); } ``` * 可以從上面的推導得知,整數有號或無號並不會影響上面的各個運算結果,因此只須將參數及回傳值型態改為 `int32_t` ## 測驗三 ### 延伸問題一 ```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 (u | v); return u << shift; } ``` 1. 當 `u` 為 0 回傳 `v` , 反之回傳 `u` , 兩者皆為 0 回傳 0 2. 當 `u` 和 `v` 皆為偶數 , $\gcd(u,v) = 2 \ast gcd({u \over 2},{v \over 2})$ , `shift` 用來記錄之後要乘回 2 多少次 3. 如果 `u` 還是偶數 , 因為 `v` 是奇數 $\gcd(u,v) = gcd({u \over 2},v)$ 4. 透過[輾轉相除法](https://zh.wikipedia.org/wiki/%E8%BC%BE%E8%BD%89%E7%9B%B8%E9%99%A4%E6%B3%95)來求出 `u` 和 `v` 的最大公因數 , 在這過程中因為 `u` 一直保持為奇數 , 因此 $\gcd(u,v) = gcd(u,{v \over 2})$ 5. 最後要將這個最大公因數乘回 2 的 `shift` 次方 註 :`while(u | v)` 應該改成 `while(v)`,否則會陷入無窮迴圈,因為 `v` 才是過程中會減到 0 的數, 而 `u` 則是最大公因數 ### 延伸問題二 透過 `__builtin_ctz` 改寫 GCD ```c uint64_t gcd64(uint64_t u, uint64_t v) { if (!u || !v) return u | v; int shift, shitf_u = __builtin_ctz(u), shift_v = __builtin_ctz(v); shift = shitf_u ^ ((shitf_u ^ shift_v) & -(shitf_u > shift_v)); u >>= shitf_u, v >>= shift_v; do { shift_v = __builtin_ctz(v); v >>= shift_v; if (u < v) { v -= u; } else { uint64_t t = u - v; u = v; v = t; } } while (v); return u << shift; } ``` ## 測驗四 ```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; } ``` * 上面函式會將 `bitmap` 資料中 , 出現 1 的位置記錄在 `out` , 並回傳 `bitmap` 出現 1 的次數 `pos` * 假設 bitmap[i] 資料為 3 個 bit * `bitmapsize` : 2 * `bitmap`: bitmap[0] = 3'b101 , bitmap[1] = 3'b010 * `out` : [0,2,4] * `pos` : 3 ```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 = bitset & -bitset; int r = __builtin_ctzll(bitset); out[pos++] = k * 64 + r; bitset ^= t; } } return pos; } ``` * 一個數的2補數會將比該數 1 的最低位元還低位的位元都變成 0 => 可知上述位元皆為 0 , 1補數變成 1 , 加一變為2補數上述位元變為 0 , 而 1 的最低位元依舊為 1 ; 高位元則會跟原數相反 * 因此可以得知 `bitset & -bitset` 會保留 `bitset` 中 1 的最低位元 , 而將其他位元變成 0 * 再透過 XOR 就可以把該位消除 , 並保留其他位元 * 相比於原本需要將 `bitmap` 資料都看過一遍 , 這裡每次就只會看位元為 1 的位置 ## 測驗五 ```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 (pos-- > 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; list_add(&node->link, &heads[remainder % size]); *q++ = (remainder * 10) / d + '0'; remainder = (remainder * 10) % d; } strcpy(p, decimal); // 沒循環小數 return result; } ``` * `rem_node->key` 儲存已經出現過的餘數,當在一次遇到同樣的餘數,表示小數循環了 * `rem_node->index` 儲存該餘數前有幾個數,這些數為小數沒循環的部分 註:`sprintf(p, "%ld", division > 0 ? (long) division : (long) -division);` 應可改為 `sprintf(p, "%ld", division);`,因為 `division = n / d` 又 `n`、`d` 皆為正數 ## 測驗六 ```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)->_h) - (char *)0) ``` * Structure Padding : a struct instance will have the alignment of its widest scalar member * 因為 struct 中 `type t` 最小也只會跟 `char` 一樣佔 1 byte,所以 struct 一定會跟 `type t` 對齊 * 因此先將 struct 地址變更為 0 ,再加上成員 _h 的偏移量就可以知道 `type t` 對齊幾個byte * pointer 的運算都是遞增或遞移 1 個「單位」, `(char *) - 1` 會減少1 byte,而 `(float *) - 1` 則會減少 4 byte,這也是為什麼要強制轉型成 `(char *)`,因為我們不知道前面是什麼pointer,而當後面不是減 0 時,可能會產生上述的錯誤 * 參考資料 * [Linux 核心原始程式碼巨集: container_of](https://hackmd.io/@sysprog/linux-macro-containerof) * [The Lost Art of Structure Packing](http://www.catb.org/esr/structure-packing/#_structure_alignment_and_padding) * [The (char *) casting in container_of() macro in linux kernel](https://stackoverflow.com/questions/20421910/the-char-casting-in-container-of-macro-in-linux-kernel) ## 測驗七 ```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 << div3) << div5; char fmt[9]; strncpy(fmt, &"FizzBuzz%u"[(9 >> div5) >> (div3 << 2)], length); fmt[length] = '\0'; printf(fmt, i); printf("\n"); } return 0; } ``` * 如果單獨為 3 或 5 的倍數字串長度 `length` 為 4 , 同時為 3 及 5 的倍數字串長度為 8,因此可得知 `length = 2 << div3 << div5` * 不為 3、5、15 的倍數印出數字本身,在字串 index 為 8 的位置,此時 `KK3` 應為 0 * 5 的倍數印出 "Buzz" ,在字串 index 為 4 的位置,即 `8 >> div5`,此時 `KK3` 應為 0 * 3 及 15 的倍數分別印出 "Fizz" 和 "FizzBuzz" ,在字串 index 為 0 的位置,兩者會使 `div3` 為 1,又 3 的倍數會使 `div5` 為 0,可知 `8 >> KK3` 應為 0,因此可知 `KK3 >= 4` 即 `div3 << 2` 註:`strncpy(fmt, &"FizzBuzz%u"[(9 >> div5) >> (div3 << 2)], length);` 中的 9 應改為 8,因 "%u" 開頭在字串 index 為 8 的位置 [題目連結](https://hackmd.io/@sysprog/linux2022-quiz2)