--- tags: linux2022 --- # 2022q1 Homework2 (quiz2) contributed by < Ahsen-lab > ## 實驗環境 ```shell $ gcc --version gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0 Copyright (C) 2019 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian Address sizes: 39 bits physical, 48 bits virtual CPU(s): 4 On-line CPU(s) list: 0-3 Thread(s) per core: 1 Core(s) per socket: 4 Socket(s): 1 NUMA node(s): 1 Vendor ID: GenuineIntel CPU family: 6 Model: 165 Model name: Intel(R) Core(TM) i5-10400 CPU @ 2.90GHz Stepping: 5 CPU MHz: 2904.002 BogoMIPS: 5808.00 Hypervisor vendor: KVM Virtualization type: full L1d cache: 128 KiB L1i cache: 128 KiB L2 cache: 1 MiB L3 cache: 48 MiB NUMA node0 CPU(s): 0-3 ``` ## 作業要求 > [作業要求](https://hackmd.io/@sysprog/BybKYf5xc) ## 測驗 `1` 對二個無號整數取平均值的程式碼: ### 程式碼運作原理 #### 實作一 ```c #include <stdint.h> uint32_t average(uint32_t a, uint32_t b) { return (a >> 1) + (b >> 1) + (a & b & 1); /* EXP1 */ } ``` ==作答區== ==`EXP1 = a & b & 1`== 兩個 `uint32_t` 無號整數 `a` 與 `b` 取平均值: $$ average = {(a + b) \over 2} = {{a \over 2} + {b \over 2}} $$ 無號整數右移一個 `bit` 其實就相當於除以 `2` 的操作,所以上述算式可改寫為 `(a >> 1) + (b >> 1)`,但是這裡要處理一個例外狀況,當 `a` 與 `b` 都是奇數的時候,`a` 與 `b` 的最低位 (最右邊的位元) 的 `1` 在右移的過程中會被消去,導致平均數會等於 `(a >> 1) + (b >> 1) + 1`。 為了要處理這個例外狀況,所以在實作中 `(a >> 1) + (b >> 1)` 會加上 `(a & b & 1)`,當 `a` 與 `b` 都是奇數時,`(a & b & 1) == 1`,如此便可處理例外的情況。 #### 實作二 ```c uint32_t average(uint32_t a, uint32_t b) { return (a & b) + ((a ^ b) >> 1); /* EXP2 & EXP3 */ } ``` ==作答區== ==`EXP2 = a & b`== ==`EXP3 = a ^ b`== 當 `a` 與 `b` 都是無號整數,`a ^ b`代表相加但不進位,`(a & b) << 1`代表進位但不相加,代入到 `(a / 2) + (b / 2)`。 `(a & b)` 代表 `(a / 2)` 和 `(b / 2)` 進位但不相加。 `(a ^ b) >> 1` 代表 `(a / 2)` 和 `(b / 2)` 相加但不進位。 所以要計算 `(a / 2) + (b / 2)` 可以代換成`(a & b) + ((a ^ b) >> 1)`。 ### 組合語言輸出 1. 未開啟最佳化 **`(a >> 1) + (b >> 1) + (a & b & 1)`** ```shell= 0x000055555555530d <+0>: endbr64 0x0000555555555311 <+4>: push %rbp 0x0000555555555312 <+5>: mov %rsp,%rbp 0x0000555555555315 <+8>: mov %edi,-0x4(%rbp) 0x0000555555555318 <+11>: mov %esi,-0x8(%rbp) 0x000055555555531b <+14>: mov -0x4(%rbp),%eax 0x000055555555531e <+17>: shr %eax 0x0000555555555320 <+19>: mov %eax,%edx 0x0000555555555322 <+21>: mov -0x8(%rbp),%eax 0x0000555555555325 <+24>: shr %eax 0x0000555555555327 <+26>: add %eax,%edx 0x0000555555555329 <+28>: mov -0x4(%rbp),%eax 0x000055555555532c <+31>: and -0x8(%rbp),%eax 0x000055555555532f <+34>: and $0x1,%eax 0x0000555555555332 <+37>: add %edx,%eax 0x0000555555555334 <+39>: pop %rbp 0x0000555555555335 <+40>: retq ``` ```shell= 0x0000555555555336 <+0>: endbr64 0x000055555555533a <+4>: push %rbp 0x000055555555533b <+5>: mov %rsp,%rbp 0x000055555555533e <+8>: mov %edi,-0x4(%rbp) 0x0000555555555341 <+11>: mov %esi,-0x8(%rbp) 0x0000555555555344 <+14>: mov -0x4(%rbp),%eax 0x0000555555555347 <+17>: and -0x8(%rbp),%eax 0x000055555555534a <+20>: mov %eax,%edx 0x000055555555534c <+22>: mov -0x4(%rbp),%eax 0x000055555555534f <+25>: xor -0x8(%rbp),%eax 0x0000555555555352 <+28>: shr %eax 0x0000555555555354 <+30>: add %edx,%eax 0x0000555555555356 <+32>: pop %rbp 0x0000555555555357 <+33>: retq ``` 2. `-O1` ```shell= 0x00005555555552fe <+0>: endbr64 0x0000555555555302 <+4>: mov %edi,%eax 0x0000555555555304 <+6>: shr %eax 0x0000555555555306 <+8>: mov %esi,%edx 0x0000555555555308 <+10>: shr %edx 0x000055555555530a <+12>: add %edx,%eax 0x000055555555530c <+14>: and %esi,%edi 0x000055555555530e <+16>: and $0x1,%edi 0x0000555555555311 <+19>: add %edi,%eax 0x0000555555555313 <+21>: retq ``` ```shell= 0x0000555555555314 <+0>: endbr64 0x0000555555555318 <+4>: mov %edi,%eax 0x000055555555531a <+6>: xor %esi,%eax 0x000055555555531c <+8>: shr %eax 0x000055555555531e <+10>: and %esi,%edi 0x0000555555555320 <+12>: add %edi,%eax 0x0000555555555322 <+14>: retq ``` 3. `-O2` ```shell= 0x00005555555552e0 <+0>: endbr64 0x00005555555552e4 <+4>: mov %edi,%eax 0x00005555555552e6 <+6>: mov %esi,%edx 0x00005555555552e8 <+8>: and %esi,%edi 0x00005555555552ea <+10>: shr %eax 0x00005555555552ec <+12>: shr %edx 0x00005555555552ee <+14>: and $0x1,%edi 0x00005555555552f1 <+17>: add %edx,%eax 0x00005555555552f3 <+19>: add %edi,%eax 0x00005555555552f5 <+21>: retq ``` ```shell= 0x0000555555555300 <+0>: endbr64 0x0000555555555304 <+4>: mov %edi,%eax 0x0000555555555306 <+6>: and %esi,%edi 0x0000555555555308 <+8>: xor %esi,%eax 0x000055555555530a <+10>: shr %eax 0x000055555555530c <+12>: add %edi,%eax 0x000055555555530e <+14>: retq ``` ## 測驗 `2` 改寫〈[解讀計算機編碼](https://hackmd.io/@sysprog/binary-representation#%E4%B8%8D%E9%9C%80%E8%A6%81%E5%88%86%E6%94%AF%E7%9A%84%E8%A8%AD%E8%A8%88)〉一文的「不需要分支的設計」一節提供的程式碼 min,我們得到以下實作 ( max ): ```c #include <stdint.h> uint32_t max(uint32_t a, uint32_t b) { return a ^ ((a ^ b) & -(a < b)); /* EXP4 & EXP5 */ } ``` ==作答區== ==`EXP4 = a ^ b`== ==`EXP5 = a < b`== 1. 首先比較 `a` 與 `b` 的大小,若 `a < b`,`-(a < b) == -(true) == -1 == 0xffffffff`,而 `(a ^ b) & 0xffffffff == (a ^ b)`,`a ^ (a ^ b) == b`。 所以若 `a < b`,則會回傳 `b`。 2. 若 `a >= b`,`-(a < b) == -(false) == -0 == 0x0`,而 `(a ^ b) & 0x0 == 0`,`a ^ 0 == a`。 所以若 `a >= b`,則會回傳 `a`。 ## 測驗 `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 (v); /* COND */ return u << shift; /* RET */ } ``` ==作答區== ==`COND = v`== ==`RET = u << shift`== 程式碼解釋: ```c=4 if (!u || !v) return u | v; ``` 檢查 `u` 或 `v` 是否為 `0`,若其中一數為 `0`,回傳另一個數,任何數與 `0` 的最大公因數就是它自己本身。 ```c=6 for (shift = 0; !((u | v) & 1); shift++) { u /= 2, v /= 2; } ``` 若 `u` , `v` 都為偶數,判斷兩數之間可以共同被整除的最大非零偶數,`shift` 代表除以幾次 `2`。 ```c=9 while (!(u & 1)) u /= 2; ``` 若 `u` 仍為偶數,將其重複除以 `2` 讓其變為奇數,因為前面已經找過兩數之間的最大偶數因數,所以不需要再考慮偶數的部分。 ```c=11 do { while (!(v & 1)) v /= 2; if (u < v) { v -= u; } else { uint64_t t = u - v; u = v; v = t; } } while (v); /* COND */ ``` 第 12~13 行,若 v 仍為偶數,將其重複除以 2 讓其變為奇數,因為前面已經找過兩數之間的最大偶數因數,所以不需要再考慮偶數的部分。 第 14~21 行,確保 `u` 比 `v` 大,用迴圈讓 `u` 除以 `v`,`u` 代表上一次相除的除數,`v` 代表上一次相除的餘數。當 `v==0` 時,`u` 會等於 `u` , `v` 兩數之間可以共同被整除的最大奇數。 ```c=22 return u << shift; /* RET */ ``` 某個無號整數左移一次代表乘以 `2`,兩數的最大公因數為 `u * 2 * shift` (`u` 代表兩數之間可以共同被整除的最大奇數),可以改寫為 `u << shift`,代表 `u` 乘以 `shift` 次 `2`。 ## 測驗 `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; } ``` 使用 `__builtin_ctzll` 改寫的程式碼如下: ```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; /* EXP6 */ int r = __builtin_ctzll(bitset); out[pos++] = k * 64 + r; bitset ^= t; } } return pos; } ``` ==作答區== ==`EXP6 = bitset & -bitset`== 設定一個變數 `pos` 用來紀錄 `bitmap` 中非零 `bit` 的個數。 第 6~14 行,使用迴圈來遍歷 `bitmap`,`bitset` 代表 `bitmap` 中每個 `index` 的值。 ```c=8 while (bitset != 0) { uint64_t t = bitset & -bitset; /* EXP6 */ int r = __builtin_ctzll(bitset); out[pos++] = k * 64 + r; bitset ^= t; } ``` 其中第 9 行的作用是找出目前最低位元的 `1`,並紀錄到 `t` 變數。舉例來說,若目前 `bitset` 為 $000101_b$,那 `t` 就會變成 $000001_b$。在二補數中,對 `bitset` 取負號表示 `~bitset + 1`,`bitset & (~bitset + 1)` 剛好可以保留最低位元的 `1`,再將結果存入 `t`。 接著使用 `__builtin_ctzll` 找出 `t` 的 Count Trailing Zeros (ctz),再將結果存入變數 `r`,並把每個非零 `bit` 的位置存入 `out` array 中,最後用 XOR 將 `bitset` 中最低位元的 `1` 清掉。 不斷重複上述的動作,最後 improved function 會回傳 `pos`,而 `out` 中會儲存所有非零 `bit` 的位置。 ## 測驗 `5` LeetCode [166. Fraction to Recurring Decimal](https://leetcode.com/problems/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 (pos-- > 0) /* PPP */ *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]); /* MMM EEE */ *q++ = (remainder * 10) / d + '0'; remainder = (remainder * 10) % d; } strcpy(p, decimal); return result; } ``` ==作答區== ==`PPP = pos--`== ==`MMM = list_add`== ==`EEE = &heads[remainder % size]`== ```c=7 struct rem_node { int key; int index; struct list_head link; }; ``` 第 7~11 行,設定 list 中的 element 的結構,包含 `key` 和 `index`。 ```c=13 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; } ``` 第 13~22 行,這裡 `heads` 代表一個 hash table,`size` 代表 hash table 的 size,`key` 代表 list 中 element 的 `key`。在 find function 中,hasa 代表包含有 `key` 的 list 的 head,使用 `list_for_each_entry` 遍歷該 list,若 list 中的 `node->key` 與 `key` 相同,則回傳 `node->index`,若沒有相同的 `key`,則回傳 `-1`。 ```c=26 int size = 1024; char *result = malloc(size); char *p = result; ``` 第 26~28 行,`result`是一段記憶體空間,用來儲存計算的結果,`p` 指向 `result` 的第一個位置。 ```c=30 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; ``` 第 30~49 行,處理分子或分母等於零的狀況,若分母為零,返回 `NULL`,分子為零返回 `0`,以及處理分子或分母為負號的狀況,若是負號則轉為正號。 ```c=51 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++ = '.'; ``` 第 51~63 行,判斷是否為負數分數,若是,result 中第一個字元為 `-`,接著處理整數的部分,將分子除以分母,把商存入 result,並在後面加入小數點 `.`。 ```c=68 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]); ``` 第 68~75 行,`decimal` 是一段記憶體空間,用來儲存小數點的部分,`q` 指向 `decimal` 的第一個位置。另外,初始化一個 size 為 1333 的 hash table,其中包含 1333 個 list。 ```c=77 for (int i = 0; remainder; i++) { int pos = find(heads, size, remainder); if (pos >= 0) { while (pos-- > 0) /* PPP */ *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]); /* MMM EEE */ *q++ = (remainder * 10) / d + '0'; remainder = (remainder * 10) % d; } ``` 第 77~97 行,使用迴圈來計算小數,首先將餘數傳入 find function 中查詢是否有重複出現的餘數: 1. 若 `pos >= 0` 代表從小數點後 `pos` 位開始出現循環小數,所以先用迴圈將小數點後到 `pos` 之間的數字填入 `result`,接著加入左括號,然後開始填入循環小數的數字,填完後加入右括號,並在結尾加上 `'\0'`,最後回傳 `result`。 2. 若 `pos < 0` 表示還未碰到循環小數,這時會初始化一個新的 `rem_node`,並將當前的 `remainder` 及 `i` 存入 `rem_node`,`i` 代表現在計算到小數點後幾位,接著用 `list_add` 將 `rem_node` 放入 hash table 中的第 `remainder % size` 個 list 的開頭,最後將 `(remainder * 10) / d` 的值存入 `decimal`,並將 `remainder` 更新為 `(remainder * 10) % d`。 ```c=99 strcpy(p, decimal); return result; ``` 第 99~100 行,若都沒有遇到循環小數,那就將 `decimal` 中的值複製到 `result` 中,並回傳 `result`。 ## 測驗 `6` `__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)->_h) - (char *)0) /* M & X */ ``` ## 測驗 `7` 考慮貌似簡單卻蘊含實作深度的 FizzBuzz 題目: * 從 1 數到 n,如果是 3 的倍數,印出 “Fizz” * 如果是 5 的倍數,印出 “Buzz” * 如果是 15 的倍數,印出 “FizzBuzz” * 如果都不是,就印出數字本身 以下是利用 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 << div3) << div5; /* KK1 & KK2 */ char fmt[9]; //strncpy(fmt, &"FizzBuzz%u"[(9 >> div5) >> (div3 << 2)], length); strncpy(fmt, &"FizzBuzz%u"[(8 >> div5) >> (div3 << 2)], length); /* KK3 */ fmt[length] = '\0'; printf(fmt, i); printf("\n"); } return 0; } ``` ==作答區== ==`KK1 = div3`== ==`KK2 = div5`== ==`KK3 = div3 << 2`== ```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; ``` `is_divisible` function 是用來判斷 `M` 是否可以被 `n` 整除,或是可用來判斷 `n` 是否是某數的倍數,判斷的公式為 `n * M <= M - 1`。 若要判斷 `n` 是否為 `x` 的倍數,則 `M` 的值為 `(uint64_t 最大值) / x + 1`。假設要判斷 `n` 是否為 `3` 的倍數,`M` 就代入 `UINT64_C(0xFFFFFFFFFFFFFFFF) / 3 + 1`。因為 `n` 一定是正整數或零,所以只有在 `n * M == 0` 時,`n * M <= M - 1` 才會成立,也就是說 `n` 一定要是 `3` 的倍數,`n * UINT64_C(0xFFFFFFFFFFFFFFFF) / 3 + 1` 才會 overflow 變成 `0`,`is_divisible` 才會返回 `true`。 ```c=9 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; /* KK1 & KK2 */ char fmt[9]; //strncpy(fmt, &"FizzBuzz%u"[(9 >> div5) >> (div3 << 2)], length); strncpy(fmt, &"FizzBuzz%u"[(8 >> div5) >> (div3 << 2)], length); /* KK3 */ fmt[length] = '\0'; printf(fmt, i); printf("\n"); } return 0; } ``` 使用迴圈判斷 1~100,若是 3 的倍數 `div3 == 1`,若是 5 的倍數 `div5 == 1`。 `length` 代表輸出字串的長度: 1. 若是 3 的倍數,`length` 等於 `(2 << 1) << 0 == 4` 2. 若是 5 的倍數,`length` 等於 `(2 << 0) << 1 == 4` 3. 若是 15 的倍數,`length` 等於 `(2 << 1) << 1 == 8` 4. 如果都不是,`length` 等於 `(2 << 0) << 0 == 2` `&"FizzBuzz%u"[(8 >> div5) >> (div3 << 2)]` 代表從第幾個字元開始的字串 (題目第 17 行的程式碼有誤,應改為上述第 18 行所示): 1. 若是 3 的倍數,`(8 >> 0) >> (1 << 2) == 0`,也就是字串 `FizzBuzz%u`。 2. 若是 5 的倍數,`(8 >> 1) >> (0 << 2) == 4`,也就是字串 `Buzz%u`。 3. 若是 15 的倍數,`(8 >> 1) >> (1 << 2) == 0`,也就是字串 `FizzBuzz%u`。 4. 如果都不是,`(8 >> 0) >> (0 << 2) == 8`,也就是字串 `%u`。 將上述參數代入 ```c strncpy(fmt, &"FizzBuzz%u"[(8 >> div5) >> (div3 << 2)], length) ``` 即可得到題目所描述的結果。