# 2020q3 Homework4 (quiz4) contributed by < `JimmyLiu0530` > ###### tags: `進階電腦系統理論與實作` ## 測驗1: LeetCode [461. Hamming Distance](https://leetcode.com/problems/hamming-distance/) 兩個整數間的 Hamming distance 為其二進位的每個位元的差。請計算輸入參數兩整數 x 與 y 的 Hamming distance,例如整數 1 的二進位為 0 0 0 1,而整數 4 的二進位為 0 1 0 0,則 1 與 4 的 Hamming distance 為 2。 ```c= int hammingDistance(int x, int y) { return __builtin_popcount(x OP y); } ``` ### 程式說明 唯有兩數的某一位元相異,他們的 hamming distance 才會加1,因此先對兩數 x, y 作 `^`,再利用 `__builtin_popcount`計算有幾個位元為1,即為 hamming distance,所以`OP = ^`。 ### 延伸問題 #### 1. 舉出不使用 gcc extension 的 C99 實作程式碼。 ```c= int hammingDistance_v2(int x, int y) { int total = 0; for (int i = 0; i < 31; i++) { int oneCount = 0; if (((1 << i) & x) != 0) oneCount++; if (((1 << i) & y) != 0) oneCount++; total += oneCount * (2 - oneCount); } return total; } ``` 仿照延伸問題2的做法,詳細的原理見下方。 #### 2. 練習 LeetCode [477. Total Hamming Distance](https://leetcode.com/problems/total-hamming-distance/),你應當提交你的實作,並確保執行結果正確且不超時 - (第一次提交)用最直覺的方法兩層 for 迴圈,結果超時 - (第二次提交)此方法採 bit-by-bit 的觀點來實作 ```c= int totalHammingDistance(int* nums, int numsSize){ int total = 0; for (int i = 0; i < 31; i++) { int oneCount = 0; for (int j = 0; j < numsSize; j++) { if (((1 << i) & nums[j]) != 0) oneCount++; } total += oneCount * (numsSize - oneCount); } return total; } ``` ![](https://i.imgur.com/utnf6Mc.png) 假設在某一位元共有 `oneCount` 個數字為1,那麼就有 `(n - oneCount)` 個數字為0,因為陣列中的每個數字皆會與其他數字計算過一次 hamming distance,所以就會使 hamming distance 總和增加 `oneCount * (n - oneCount)`。如此一來,遍歷所有位元的總和即為最後的答案。 #### 3. 研讀[錯誤更正碼簡介](https://w3.math.sinica.edu.tw/math_media/d184/18404.pdf)及[抽象代數的實務應用](https://www.math.sinica.edu.tw/www/file_upload/summer/crypt2017/data/2017/%E4%B8%8A%E8%AA%B2%E8%AC%9B%E7%BE%A9/[20170710][%E6%8A%BD%E8%B1%A1%E4%BB%A3%E6%95%B8%E7%9A%84%E5%AF%A6%E5%8B%99%E6%87%89%E7%94%A8].pdf),摘錄其中 Hamming Distance 和 Hamming Weight 的描述並重新闡述,舉出實際的 C 程式碼來解說 > - 搭配閱讀 [Hamming codes, Part I](https://www.youtube.com/watch?v=X8jsijhllIA&feature=youtu.be) 及 [Hamming codes, Part II](https://www.youtube.com/watch?v=b3NxrZOu_CE&feature=youtu.be),你可適度摘錄影片中的素材,但應當清楚標註來源 Hamming weight, 記作 $w_H(x)$, 表示 x 向量有幾個位元為1; 而 Hamming distance, 記作 $d_H(x,x')$, 表示兩個向量有幾個不同的位元,又可透過相加兩向量再取其 hamming weight 得到,因此可以寫成 $w_H(x+x')$ (即 $d_H(x,x')$ = $w_H(x+x')$)。 假設模擬從 sender 取得的16個位元,其中11個 data bits,4個 parity bits: ```c= char bits[16]; for (int i = 0; i < 16; i++) { bits[i] = rand()%2; } /*output:{1,0,0,0,1,0,1,0,0,1,1,1,0,1,1,0}*/ ``` 接著針對所有位元為1的作 XOR,會得到: ```c= int res = 0; for (int i = 0; i < 16; i++) { if (bits[i]) res ^= bits[i]; } /*output: 9*/ ``` 為了讓上述的 XOR 結果為0,9的二進位為`1001`,因此對第 1 及第 8 個進行位元反轉: ```c= bits[1] = !bits[1]; bits[8] = !bits[8]; int res = 0; for (int i = 0; i < 16; i++) { if (bits[i]) res ^= bits[i]; } /*output: 0*/ ``` 接下來我們就可以模擬資料傳送過程中受干擾的情況,例如假設第 10 個位元在傳送中受到干擾,XOR 結果就會得到 10,也就是錯誤的位元,因此可以馬上找到錯誤的位元加以修正: ```c= bits[10] = !bits[10]; int res = 0; for (int i = 0; i < 16; i++) { if (bits[i]) res ^= bits[i]; } /*output: 10*/ ``` #### 4. 研讀 [Reed–Solomon error correction](https://en.wikipedia.org/wiki/Reed%E2%80%93Solomon_error_correction),再閱讀 Linux 核心原始程式碼的 [lib/reed_solomon 目錄](https://github.com/torvalds/linux/tree/master/lib/reed_solomon),抽離後者程式碼為單獨可執行的程式,作為 ECC 的示範 -------------------------------------- ## 測驗2: LeetCode [1483. Kth Ancestor of a Tree Node](https://leetcode.com/problems/kth-ancestor-of-a-tree-node/) 給定一棵樹,樹上有 n 個節點,編號自 0 到 n−1。樹以父節點陣列的形式提供,其中 parent[i] 是節點 i 的父節點。樹的根節點是編號為 0 的節點。請設計 treeAncestorGetKthAncestor(TreeAncestor *obj, int node, int k) 函式,回傳節點 node 的第 k 個祖先節點。若不存在這樣的祖先節點,回傳 -1。樹節點的第 k 個祖先節點是從該節點到根節點路徑上的第 k 個節點 ```c= #include <stdlib.h> typedef struct { AAA; int max_level; } TreeAncestor; TreeAncestor *treeAncestorCreate(int n, int *parent, int parentSize) { TreeAncestor *obj = malloc(sizeof(TreeAncestor)); obj->parent = malloc(n * sizeof(int *)); int max_level = 32 - __builtin_clz(n) + 1; for (int i = 0; i < n; i++) { obj->parent[i] = malloc(max_level * sizeof(int)); for (int j = 0; j < max_level; j++) obj->parent[i][j] = -1; } for (int i = 0; i < parentSize; i++) obj->parent[i][0] = parent[i]; for (int j = 1;; j++) { int quit = 1; for (int i = 0; i < parentSize; i++) { obj->parent[i][j] = obj->parent[i][j + BBB] == -1 ? -1 : obj->parent[obj->parent[i][j - 1]][j - 1]; if (obj->parent[i][j] != -1) quit = 0; } if (quit) break; } obj->max_level = max_level - 1; return obj; } int treeAncestorGetKthAncestor(TreeAncestor *obj, int node, int k) { int max_level = obj->max_level; for (int i = 0; i < max_level && node != -1; ++i) if (k & (CCC)) node = obj->parent[node][i]; return node; } void treeAncestorFree(TreeAncestor *obj) { for (int i = 0; i < obj->n; i++) free(obj->parent[i]); free(obj->parent); free(obj); } ``` ### 程式說明 首先,根據 line 10-11及15得知 `obj->parent` 是一個指標的指標,因此 `AAA = int **parent`。 此題的原理: 假設要找某節點的第 k 個祖先,k = 3 (= $2^0+2^1$) 為例,那麼意同於找此節點的第 $2^0$ 個祖先的第 $2^1$ 個祖先。 > **為甚麼要這樣做?** > 因為 n 個點的樹在最差的情況下有 n-1 個祖先,若將每個節點從第 0 到第 n-1 個祖先記錄下來,則需要 O($n^2$) 的空間,但取得第 k 個祖先只需要 O(1) 的時間。 > > 相反的,若不額外記錄各節點的祖先,時間複雜度就會提高。因此本方法能在時間與空間取一個平衡 - 僅記錄第 $2^0$、$2^1$、...、$2^n$ 個祖先。 obj->parent[i][j] 的意義就是第 i 個節點的第 $2^j$ 個祖先是誰,所以 line 14-20 就在對此二維陣列初始化及設定每個節點的第一個祖先。 接著,line 22-31 建立每個節點 i 的第 $2^j$ 個祖先,會先看此節點是否存在第 $2^{j-1}$ 個祖先, - 若不存在,則此節點必不存在第 $2^j$ 個祖先; - 若存在,則此節點的第 $2^j$ 個祖先 = 此節點的第 $2^{j-1}$ 個祖先的第 $2^{j-1}$ 個祖先,因此 `BBB = -1`。 最後呼叫 `treeAncestorGetKthAncestor` 時,根據上述第二段的原理去拆解 k ,因此 `CCC = 1 << i`。 ### 延伸問題 #### 1. 指出可改進的地方,並實作對應的程式碼 - Compile error 在 `TreeAncestor` struct 中,新增 `int n;` ```c= typedef struct { ... int n; } TreeAncestor; ``` 並在 `treeAncestorCreate` 中,新增 `obj->n = n;` ```c= TreeAncestor *treeAncestorCreate(int n, int *parent, int parentSize) { TreeAncestor *obj = malloc(sizeof(TreeAncestor)); obj->parent = malloc(n * sizeof(int *)); obj->n = n; } ``` - 減少使用的記憶體 在原本的程式碼中,之所以 `max_level = 32 - __builtin_clz(n) + 1;` 需要 `+1` ,為的就是多額外配置空間給 obj->parent[i] ,好讓底下 line 22 的 `for` 不會越界存取。(因為這裡 `for` 是靠 `quit` 來離開迴圈的) 也就是說若新增 for 的中止條件,就不用擔心越界,也不需要配置額外空間,來達到節省記憶體的目的。 因此,修改後的部分程式碼如下: ```c= TreeAncestor *treeAncestorCreate(int n, int *parent, int parentSize) { ... int max_level = 32 - __builtin_clz(n); // remove +1 ... for (int j = 1; j < max_level; j++) { // add j < max_level int quit = 1; for (int i = 0; i < parentSize; i++) { obj->parent[i][j] = obj->parent[i][j - 1] == -1 ? -1 : obj->parent[obj->parent[i][j - 1]][j - 1]; if (obj->parent[i][j] != -1) quit = 0; } if (quit) break; } obj->max_level = max_level; // remove +1 return obj; } ``` ![](https://i.imgur.com/FOmJocN.png) 修改前的程式碼需要 70.3 MB 的記憶體空間,而修改過後只需 68.5 MB,減少了 1.8 MB。 #### 3. 在 LeetCode [1483. Kth Ancestor of a Tree Node](https://leetcode.com/problems/kth-ancestor-of-a-tree-node/) 提交你的實作,你應該要在 C 語言項目中,執行時間領先 75% 的提交者 -------------------------------------- ![](https://i.imgur.com/s6Ckfns.png) ## 測驗3: [FizzBuzz](https://en.wikipedia.org/wiki/Fizz_buzz) 考慮貌似簡單卻蘊含實作深度的 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= #define MSG_LEN 8 char fmt[MSG_LEN + 1]; 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= #define MSG_LEN 8 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[MSG_LEN + 1]; strncpy(fmt, &"FizzBuzz%u"[(MSG_LEN >> KK1) >> (KK2 << KK3)], length); fmt[length] = '\0'; printf(fmt, i); printf("\n"); } return 0; } ``` gcc-9 還內建了 [FizzBuzz optimization](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=82853) (Bug 82853 - Optimize x % 3 == 0 without modulo)。 ### 程式說明 首先,回顧一下 quiz 2 所學的[快速 mod 運算](https://hackmd.io/JMzU3vDsRYuavmBRvpPjDA#%E6%B8%AC%E9%A9%973-%E5%BF%AB%E9%80%9Fmod%E9%81%8B%E7%AE%97)即可了解 line 3-4, 7-8 及 12-13 的原理,在此不贅述。 根據題目給的提示可以整理成下列四種可能: | 被n整除 | start | length | | ------ | ----- | ------ | | n = 3 | 0 | 4 | | n = 5 | 4 | 4 | | n = 15 | 0 | 8 | | X | 8 | 2 | (X:不被3或5整除) 再回頭看程式碼,在 line 12-13,`div3` 及 `div5` 若為 1,代表 `i` 分別被 3 跟 5 整除,否則為0,於是根據上面表格可以推算出 `length = (2 << div3) << div5`。 處理完 `length` 接著換 `start`。在 line 17,此處`(MSG_LEN >> KK1) >> (KK2 << KK3)`即為 `start`,一樣根據上表,即可推得 `KK1 = div5`, `KK2 = div3`, `KK3 = 2`。 ### 延伸問題 #### 1. 評估 `naive.c` 和 `bitwise.c` 效能落差 > - 避免 stream I/O 帶來的影響,可將 `printf` 更換為 `sprintf` #### 2. 分析 [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/) 一文的想法 (可參照同一篇網誌下方的評論),並設計另一種 bitmask,如「可被 3 整除則末位設為 1」「可被 5 整除則倒數第二位設定為 1」,然後再改寫 `bitwise.c` 程式碼,試圖運用更少的指令來實作出 branchless; > - 參照 [fastmod: A header file for fast 32-bit division remainders on 64-bit hardware](https://github.com/lemire/fastmod) -------------------------------------- ## 測驗4: 已知除數範圍,加速除法運算 考慮到整數 `PAGE_QUEUES` 可能有以下數值: - (1 or 2 or 3 or 4) - (5 or 6 or 7 or 8) x $2^n$, n from 1 to 14 給定 `size_t offset`,試著改善原本運算: ```c= #include <stdint.h> size_t blockidx = offset / PAGE_QUEUES; ``` 由於 `PAGE_QUEUES` 的數值分佈已知,在整數除法時,可思考加速運算的機制。需要注意,某些硬體平台缺乏整數除法指令 (如 Arm Cortex-A8),即使 Intel 公司出品的 Core 處裡器 [Sandy Bridge 微架構](https://en.wikipedia.org/wiki/Sandy_Bridge)中,針對 64 位元整數的除法運算,會佔用 40 到 103 個處理器週期,運算成本仍屬沉重。 > 來源: [Agner Fog’s instruction tables](https://www.agner.org/optimize/instruction_tables.pdf),第 180 頁 於是我們可預先進行計算,從而避免整數除法指令的使用: (假設在 x86_64 硬體執行 GNU/Linux 並透過 gcc/clang 編譯) ```c= #include <stdint.h> #define ffs32(x) ((__builtin_ffs(x)) - 1) size_t blockidx; size_t divident = PAGE_QUEUES; blockidx = offset >> ffs32(divident); divident >>= ffs32(divident); switch (divident) { CASES } ``` 其中 CASES 可能形如: ```c= case 2: blockidx /= 2; break; ``` 這樣的組合,請用最少的 case-break 敘述做出同等`size_t blockidx = offset / PAGE_QUEUES;` 效果的程式碼。 ### 程式說明 此方法原理就是先對被除數以及除數同時右移 `ffs32(divident)`位元,剩下的運算再交給 case 處理。 | divident(dec) | divident(binary) | divident >> ffs(divident) | | -------- | ------------------------- | ----- | | 1*$2^n$ | 00010...0 | 0001=1 | | 2*$2^n$ | 00100...0 | 0001=1 | | 3*$2^n$ | 00110...0 | 0011=3 | | 4*$2^n$ | 01000...0 | 0001=1 | | 5*$2^n$ | 01010...0 | 0101=5 | | 6*$2^n$ | 01100...0 | 0011=3 | | 7*$2^n$ | 01110...0 | 0111=7 | | 8*$2^n$ | 10000...0 | 0001=1 | 根據上面張表可得知 CASES 至少需要包含 1, 3, 5, 7 這四個數字,但又因為任何數除以 1 還是原數,所以只需 3, 5, 7 三數即可。 ### 延伸問題 #### 1. 練習 LeetCode [51. N-Queens](https://leetcode.com/problems/n-queens/),應選擇 C 語言並善用上述的 `__builtin_ffs`,大幅降低記憶體開銷 ```c= int ways[] = {1, 1, 0, 0, 2, 10, 4, 40, 92, 352, 724, 2680, 14200, 73712, 365596, 2279184, 14772512, 95815104, 666090624}; int *QueenPos; int ***res; int count = 0; void StoreAnswer(int n) { char* queen = malloc((n + 1) * sizeof(char)); queen[n] = '\0'; for (int i = 0; i< n; i++){ for(int j = 0; j< n; j++){ queen[j] = (QueenPos[i] == j) ? 'Q' : '.'; } memcpy(res[count][i], queen, n + 1); } free(queen); count++; } bool checkQueen(int row, int col) { for (int i = 0; i < row; i++) { if (col == QueenPos[i] || abs(col - QueenPos[i]) == abs(row - i)) return false; } return true; } void NQueens(int row, int n) { if (count == ways[n]) return; for (int j = 0; j < n; j++) { if (checkQueen(row, j)) { QueenPos[row] = j; if (row == n - 1) StoreAnswer(n); else NQueens(row + 1, n); } } } char ***solveNQueens(int n, int *returnSize, int **returnColumnSizes) { *returnSize = ways[n]; *returnColumnSizes = malloc(ways[n] * sizeof(int)); for (int i = 0; i < ways[n]; i++) { (*returnColumnSizes)[i] = n; } QueenPos = malloc(n * sizeof(int)); res = malloc(ways[n] * sizeof(char **)); for (int i = 0; i < ways[n]; i++) { res[i] = malloc(n * sizeof(char *)); } for (int i = 0; i < ways[n]; i++) { for (int j = 0; j < n; j++) { res[i][j] = malloc((n + 1) * sizeof(char)); } } NQueens(0, n); count=0; return res; } ``` 程式說明: - 大致流程(可搭配下面的圖來看): - 從第0列開始,一列一列往下看。 - 每一列中,由左至右一行一行看,透過呼叫 `CheckQueen` 檢查是否 Queen 可以放在此位置 - 若可以,則遞迴的往下一列去看(即`NQueens(row+1)`) - 這裡還要檢查是否為最後一列,若是,則以找到其中一種棋盤擺放方式,呼叫 `PushAnswer` 將結果存入 `res` - 若已找到全部的擺放方式,則 return ```graphviz digraph{ node [shape = record] n1[label="{Q|x|x|x}|{x|x||}|{x||x|}|{x|||x}"] n2[label="{x|Q|x|x}|{x|x|x|}|{|x||x}|{|x||}"] n3[label="{x|x|Q|x}|{|x|x|x}|{x||x|}|{||x|}"] n4[label="{x|x|x|Q}|{||x|x}|{|x||x}|{x|||x}"] n1302[label="{Q|x|x|x}|{x|x|x|Q}|{x|Q|x|x}|{x|x|x|x}" color = gray] n14[label="{Q|x|x|x}|{x|x||x}|{x|x|x|}|{x|Q|x|x}"] n24[label="{x|x||}|{Q|x|x|x}|{x|x|x|}|{x|Q|x|x}"] n31[label="{x|Q|x|x}|{x|x|x|}|{Q|x|x|x}|{x|x||}"] n41[label="{x|Q|x|x}|{x|x|x|}|{x|x||x}|{Q|x|x|x}"] n4203[label="{x|x|x|x}|{x|Q|x|x}|{x|x|x|Q}|{Q|x|x|x}" color = gray] n1420[label="{Q|x|x|x}|{x|x|Q|x}|{x|x|x|x}|{x|Q|x|x}" color = gray] n1403[label="{Q|x|x|x}|{x|x|x|x}|{x|x|x|Q}|{x|Q|x|x}" color = gray] n2413[label="{x|x|Q|x}|{Q|x|x|x}|{x|x|x|Q}|{x|Q|x|x}" color= red] n2401[label="{x|x|x|Q}|{Q|x|x|x}|{x|x|x|x}|{x|Q|x|x}" color = gray] n3142[label="{x|Q|x|x}|{x|x|x|Q}|{Q|x|x|x}|{x|x|Q|x}" color= red] n3104[label="{x|Q|x|x}|{x|x|x|x}|{Q|x|x|x}|{x|x|x|Q}" color = gray] n4130[label="{x|Q|x|x}|{x|x|x|x}|{x|x|Q|x}|{Q|x|x|x}" color = gray] n4102[label="{x|Q|x|x}|{x|x|x|Q}|{x|x|x|x}|{Q|x|x|x}" color = gray] root->n1->n1302 n1->n14->n1420 n14->n1403 root->n2->n24->n2413 n24->n2401 root->n3->n31->n3142 n31->n3104 root->n4->n41->n4130 n41->n4102 n4->n4203 } ``` - `***solveNQueens(int n, int *returnSize, int **returnColumnSizes)` - `*returnSize`: 記錄 `res` 的 2D array 數量(即 `ways[n]`) - `**returnColumnSizes`: 記錄每個 2D array 的行數(即 `n`) - `QueenPos`: 記錄各列 Queen 所在的行數(即 QueenPos[1]=3 代表 Queen在第一列第三行) - `***res`: 記錄每個答案的棋盤擺放方式 - `NQueens(int row, int n)` - 主要就是**遞迴**的去找可能的擺放方式 - line 34-35 用來判斷是否已找到全部擺放方式 - `checkQueen(int row, int col)` - 檢查該 `row` 以上的所有格子看是否已存在 Queen 使得 1. 與 `col` 同一行 或 2. 在 `(row, col)` 的右上或左上 - 若都沒有,代表 `(row, col)` 可以放 Queen,反之,則不行放 - `StoreAnswer(int n)` - 根據 `QueenPos` 將一種擺放方式存到 `res` 中 - 看過所有 n x n 個格子,若該格有擺放 Queen,則為 `'Q'`,否則為 `'.'`