--- title: '2020q3 Homework4 (quiz4)' tags: linux2020 --- # 2020q3 Homework4 (quiz4) contributed by < `ChongMingWei` > ## Outline [TOC] ## 環境 ```shell $ uname -a Linux cmw-System-Product-Name 5.4.0-47-generic #51~18.04.1-Ubuntu SMP Sat Sep 5 14:35:50 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux $ gcc --version gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0 Copyright (C) 2017 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. ``` ## 作業要求 ## 測驗 1 要計算每個位元的差,就使用 `XOR` 來檢查哪些 bit 不是同時為 set 或是 clear ,最後在使用 popcount 來計算 set bit 的數量。 ```c= int hammingDistance(int x, int y) { return __builtin_popcount(x ^ y);//OP } ``` :::success 1.解釋上述程式碼運作原理,並舉出不使用 gcc extension 的 C99 實作程式碼; ::: - Line 5 可得到最低位的 set bit ,其他 bits 設為 clear 。 - Line 6 計算 set bit 的數量 - Line 7 計算完 set bit 的數量,將剛剛得到的最低位 set bit 用 `XOR` 來消除 ```c= int hammingDistance(int x, int y){ int a = x^y; int b = 0; while (a){ int t = a & (-a); b++; a ^= t; } return b; //return __builtin_popcount(x ^ y); } ``` :::success 2.練習 [LeetCode 477. Total Hamming Distance](https://leetcode.com/problems/total-hamming-distance/),你應當提交 (submit) 你的實作 (C 語言實作),並確保執行結果正確且不超時 ::: - Version 1: Using double for loop to find hamming distance of each pair. - Result: Time Limit Exceeded ```c= int totalHammingDistance(int* nums, int numsSize){ int sum=0; for (int i = 0;i < numsSize - 1; ++i){ for (int j = i + 1; j < numsSize; ++j){ sum += __builtin_popcount(nums[i]^nums[j]); } } return sum; } ``` - Version 2: Replace the `__builtin_popcount` by constant time implementation - Result ![](https://i.imgur.com/RyRrAbb.png) ```c= int totalHammingDistance(int* nums, int numsSize){ int sum=0; for (int i = 0;i < numsSize - 1; ++i){ for (int j = i + 1; j < numsSize; ++j){ int a = nums[i]^nums[j]; unsigned n; unsigned v = (unsigned)a; n = (v >> 1) & 0x77777777; v -= n; n = (n >> 1) & 0x77777777; v -= n; n = (n >> 1) & 0x77777777; v -= n; v += v >> 4; v &= 0x0F0F0F0F; v *= 0x01010101; v >>= 24; sum +=v; } } return sum; } ``` - Version3: For each bit, we can find the hamming distance of each pair is `countOfOne*countOfZero`. So we traverse 32 bits and computes the hamming distance of each one bit. Finally, sum all hamming distance. - Result ![](https://i.imgur.com/NTslLha.png) > 參考 [O(n) Simple Solution](https://leetcode.com/problems/total-hamming-distance/discuss/865629/O(n)-Simple-Solution) ```c= int totalHammingDistance(int* nums, int numsSize){ int sum=0; for(int i = 0; i < 32;++i){ int count1=0; for (int j = 0;j < numsSize;++j) count1 += (nums[j]&(1u<<i))>>i;//if bit i of nums[j] is 1, count1++ sum += count1*(numsSize-count1);//compute hamming distance of bit i } return sum; } ``` ## 測驗 2 > 參考 [YLowy](https://hackmd.io/@YLowy/S1PmArmLD) 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 { int **parent;//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 + (-1)] == -1//BBB ? -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 & (1<<i))//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); } ``` ### 破題 根據題意,首先面臨的問題就是要建立怎麼樣的一個資料結構,來儲存如以下圖的 tree: ![](https://i.imgur.com/AVnjv3f.png) 在此實作中,我們採用的是查表的方式,建立一個名為 `TreeAncestor` 的 struct ,使用裡面的表格 `parent` ,來供我們查詢,那麼查詢的方法是甚麼呢? ### 查詢方法 我們會建立一個 $node\times (\lceil{log_{_2}node}\rceil+1)$ (此處 node=7)大小的表格,以上圖為範例: | Node | 2^0^ th ancestor | 2^1^ th ancestor | 2^2^ th ancestor | | | -------- | -------- | -------- | -------- | -------- | | 0 | | | | | | 1 | | | | | | 2 | | | | | | 3 | | | | | | 4 | | | | | | 5 | | | | | | 6 | | | | | 這邊其實多出一個 column ,==用途是讓我們有修改的空間== 在表格中, `parent[node][j]` 中儲存的是 `node` 的 2^j^ th 祖先,我們把要找第 k 個祖先的 "k" 拿出來討論: k 是一個正整數,而正整數 k 事實上可以被表示成數個 2^n^ 的和 E.g. 要找第 7 個祖先,就是去找 ((第 1 個祖先) 的第 2 個祖先) 的第 4 個祖先 >因為 7 = 2^0^ + 2^1^ + 2^2^ 這樣一來,我們就可以達到 O(logn) 的搜尋時間 > 假設我們今天的資料結構是用 linked list ,每個 node 都有著其 parent 的位址,那麼我們的搜尋時間就會是 O(n) ### Code Details - 計算樹的高度以及將表格 `parent` 初始化,全部都設成-1 ```c=13 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; } ``` | Node | 2^0^ th ancestor | 2^1^ th ancestor | 2^2^ th ancestor | | | -------- | -------- | -------- | -------- | -------- | | 0 | -1 | -1 | -1 | -1 | | 1 | -1 | -1 | -1 | -1 | | 2 | -1 | -1 | -1 | -1 | | 3 | -1 | -1 | -1 | -1 | | 4 | -1 | -1 | -1 | -1 | | 5 | -1 | -1 | -1 | -1 | | 6 | -1 | -1 | -1 | -1 | - 將每一個 node 的父親填入表格中 ```c=19 for (int i = 0; i < parentSize; i++) obj->parent[i][0] = parent[i]; ``` | Node | 2^0^ th ancestor | 2^1^ th ancestor | 2^2^ th ancestor | | | -------- | -------- | -------- | -------- | -------- | | 0 | -1 | -1 | -1 | -1 | | 1 | 0 | -1 | -1 | -1 | | 2 | 0 | -1 | -1 | -1 | | 3 | 1 | -1 | -1 | -1 | | 4 | 1 | -1 | -1 | -1 | | 5 | 2 | -1 | -1 | -1 | | 6 | 2 | -1 | -1 | -1 | - 將表格 `parent` 中,剩下的欄位填入資料 - Line 25: 如果第2^n-1^個祖先不存在 (-1),那麼後續第2^n^個祖先也不存在 (設為-1) - Line 27: 如果第2^n-1^個祖先存在 (!= -1),那麼第2^n^個祖先就是**第2^n-1^個祖先的第2^n-1^個祖先**。 > 2^n^ = 2^n-1^ + 2^n-1^ ```c=22 for (int j = 1;; j++) { int quit = 1; for (int i = 0; i < parentSize; i++) { obj->parent[i][j] = obj->parent[i][j + (-1)] == -1//BBB ? -1 : obj->parent[obj->parent[i][j - 1]][j - 1]; if (obj->parent[i][j] != -1) quit = 0; } if (quit) break; } ``` | Node | 2^0^ th ancestor | 2^1^ th ancestor | 2^2^ th ancestor | | | -------- | -------- | -------- | -------- | -------- | | 0 | -1 | -1 | -1 | -1 | | 1 | 0 | -1 | -1 | -1 | | 2 | 0 | -1 | -1 | -1 | | 3 | 1 | 0 | -1 | -1 | | 4 | 1 | 0 | -1 | -1 | | 5 | 2 | 0 | -1 | -1 | | 6 | 2 | 0 | -1 | -1 | - 樹的高度,前面 Line 13 沒必要+1的(會使得表格多出一個欄位),這邊再-1也就顯得多餘。 ```c=32 obj->max_level = max_level - 1; ``` - 搜尋第 k 個祖先 - Line 38: 先得到樹的最大高度 - Line 39: for 迴圈的跳出條件中, i 的搜尋次數限定不超過 `max_level` 且判斷目前的 node 是否已經是 root (-1) - Line 40: if 使用 k 的二進位表示法來判斷是否要去找之前的祖先 > E.g. > k = 3, 表示要找第1個祖先的第2個祖先, > 因為3 = 2^0^ + 2^1^,會在 i = 0和1 的時候更新 node 的值 > k = 5, 表示要找第1個祖先的第4個祖先, > 因為5 = 2^0^ + 2^2^,會在 i = 0和2 的時候更新 node 的值 ```c=38 int max_level = obj->max_level; for (int i = 0; i < max_level && node != -1; ++i) if (k & (1<<i))//CCC node = obj->parent[node][i]; return node; ``` -Result ![](https://i.imgur.com/GTr7bDS.png) :::success 2. 在 treeAncestorCreate 函式內部,若干記憶體被浪費,請撰寫完整測試程式碼,並透過工具分析 ::: :::success 3. 在 [LeetCode 1483. Kth Ancestor of a Tree Node](https://leetcode.com/problems/kth-ancestor-of-a-tree-node/) 提交你的實作,你應該要在 C 語言項目中,執行時間領先 75% 的提交者 ::: - Line 13-14: 修改之前發現建立表格 `parent` 時多出來的一個欄位,也就是移除計算 `max_level` 時多餘的部分 - Line 24-31: 之前在建立表格的最後一個步驟時,是每一個欄位都會更新一次,將其更改為,在更新每一個 node 的祖先欄位時,發現出現 -1 就跳出該個迴圈。 | Node | 2^0^ th ancestor | 2^1^ th ancestor | 2^2^ th ancestor | | -------- | -------- | -------- | -------- | | 0 | -1 | -1 | -1 | | 1 | 0 | -1 | -1 | | 2 | 0 | -1 | -1 | | 3 | 1 | 0 | -1 | | 4 | 1 | 0 | -1 | | 5 | 2 | 0 | -1 | | 6 | 2 | 0 | -1 | ```c= #include <stdlib.h> typedef struct { int **parent;//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); obj->max_level = max_level; 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 i = 0; i < parentSize; i++) { for (int j = 1;j<max_level; j++) { if(obj->parent[i][j + (-1)] == -1) break; else obj->parent[i][j] = obj->parent[obj->parent[i][j - 1]][j - 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 & (1<<i))//CCC node = obj->parent[node][i]; return node; } void treeAncestorFree(TreeAncestor *obj) { for (int i = 0; i < obj->max_level; i++) free(obj->parent[i]); free(obj->parent); free(obj); } ``` ![](https://i.imgur.com/ogGmvYR.png) ## 測驗 3 考慮貌似簡單卻蘊含實作深度的 [FizzBuzz](https://en.wikipedia.org/wiki/Fizz_buzz) 題目: + 從 1 數到 n,如果是 3的倍數,印出 “Fizz” + 如果是 5 的倍數,印出 “Buzz” + 如果是 15 的倍數,印出 “FizzBuzz” + 如果都不是,就印出數字本身 直覺的實作程式碼如下: (`naive.c`) ```cpp #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; } ``` 利用 bitwise 和由 i 來控制字串起始和長度所實作的 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); // 如果是3或5的倍數,要輸出的長度皆為4,如果是15的倍數,要輸出長度為8 unsigned int length = (2 << div3) << div5; char fmt[MSG_LEN + 1]; strncpy(fmt, &"FizzBuzz%u"[(MSG_LEN >> div5) >> (div3 << 2)], length); fmt[length] = '\0'; printf(fmt, i); printf("\n"); } return 0; } ``` - 根據是否為3, 5, 15的倍數,分別給予不同的起始 index 。 ```c=18 strncpy(fmt, &"FizzBuzz%u"[(MSG_LEN >> KK1) >> (KK2 << KK3)], length); ``` - 可以整理成以下的表格 | div3 | div5 |start | | -------- | -------- | -------- | | 0 | 0 | 8 | | 0 | 1 | 4 | | 1 | 0 | 0 | | 1 | 1 | 0 | - 從`8`開始的,表示既不是3的倍數,也不是5的倍數 - 從`4`開始的,表示只能是5的倍數 - 從`0`開始的,表示可能是15的倍數,也可能只是3的倍數,這邊要輸出甚麼樣的字串是由 Line 15 計算長度時所控制,所以把他當成同一種狀況就好。 - 回到程式碼,`KK1` 要填的就是 `div5` ,表示在只有能被5整除時,起始點為 `8>>1`(也就是4) - 而後面的 `(KK2 << KK3)` 則需要考慮,如果能被3整除時要讓起始點變成0,反之要能讓起始點保持在8,當我們填 `(div3 << 2)` 就滿足了以上的考慮。 :::success 1. 解釋上述程式運作原理並評估 naive.c 和 bitwise.c 效能落差 - 避免 stream I/O 帶來的影響,可將 printf 更換為 sprintf ::: - 實驗設計: i 從 1 到 100,將每次執行的時間相加後,最後除以100 - 使用 printf 來做實驗 (**加速 1.21x**) | Naive | Bitwise | | -------- | -------- | | 10601 ns | 8769 ns | - 使用 sprintf 來做實驗 (**加速 1.69x**) | Naive | Bitwise | | -------- | -------- | | 584 ns | 345 ns | 可以見得,做 printf 會比 sprintf 耗時,會消耗掉改成 bitwise 操作所加速的時間。 ## 測驗 4 測驗4介紹了一個 GCC 的擴充函式 `int __builtin_ffs (int x)` ,說明如下: >摘自 [Built-in Functions Provided by GCC](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html) >Built-in Function: int __builtin_ffs (int x) Returns one plus the index of the least significant 1-bit of x, or if x is zero, returns zero. 也就是說這個函式返回的值是最低位 set bit 的 index 值再加上1 > E.g. > $6_{10} = 110_2$ > $\_\_builtin\_ffs (6) = 2(index\ of\ the\ least\ significant\ set\ bit = 1)$ 回到題目,這題是在事先知道除數 (PAGE_QUEUES) 有哪些的情況下,要將原本的整數除法修改成兩步驟: 1. 先除以除數中的因數:2^n^ (使用 right shift 來做) 2. 再除以除數中的剩餘因數 PAGE_QUEUES 如下: - (1 or 2 or 3 or 4) - (5 or 6 or 7 or 8) × (2^n^), n from 1 to 14 給定 `size_t offset`,試著將原本運算: ```cpp #include <stdint.h> size_t blockidx = offset / PAGE_QUEUES; ``` 更改成 ```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) { case 3: blockidx /= 3; break; case 5: blockidx /= 5; break; case 7: blockidx /= 7; break; } ``` 前面有敘述過 `int __builtin_ffs (int x)` 返回值,這邊的 `ffs32(x)` 做的就是計算最低位 set bit 的 index ,**代表著這個數 x 有 2^ffs32(x)^ 這個因數**。 ```c=2 #define ffs32(x) ((__builtin_ffs(x)) - 1) ``` 在看這兩行之前,我們先觀察上述 PAGE_QUEUES 的數值: - (1 or 2 or 3 or 4) - (5 or 6 or 7 or 8) × (2^n^), n from 1 to 14 我們將其做整理: | Original | After | | -------- | -------- | | 1 | 2^0^ | | 2 | 2^1^ | | 3 | 3 x 2^1^ | | 4 | 2^2^ | | 5 x 2^n^ n from 1 to 14 | 5 x 2^n^ n from 1 to 14 | | 6 x 2^n^ n from 1 to 14 | 3 x 2^n+1^ n from 1 to 14 | | 7 x 2^n^ n from 1 to 14 | 7 x 2^n^ n from 1 to 14 | | 8 x 2^n^ n from 1 to 14 | 2^n+3^ n from 1 to 14 | 而這是我們要計算的值: $\dfrac{offset}{PAGE\_QUEUES}$ 我們可以同時對分子分母除以2^n^(n = ffs32(divident)),也就是 Line 5-6 在做的事情 ```c=5 blockidx = offset >> ffs32(divident); divident >>= ffs32(divident); ``` 而根據上面表格,當我們做完 分子分母同時 right shift `ffs32(divident)`之後,會有4種情況: 1. divident = 1 2. divident = 3 3. divident = 5 4. divident = 7 所以我們要接著再做除數為 `3`, `5`, `7`的除法(`1`就不用繼續做了) ```c=7 switch (divident) { case 3: blockidx /= 3; break; case 5: blockidx /= 5; break; case 7: blockidx /= 7; break; } ```