# 2022q1 Homework3 (quiz3) contributed by < `Kevin-Shih` > ## 測驗 1 **題目** 在 Linux 核心原始程式碼,[include/linux/bitfield.h](https://github.com/torvalds/linux/blob/master/include/linux/bitfield.h) 提及一個巨集 `GENMASK`,其作用是依據給定的範圍,產生連續的 bitmask,例如: - `GENMASK(6, 4)` 產生 01110000~2~ - `GENMASK(39, 21)` 產生 0x000000ffffe00000 (64 位元) 已知我們使用的微處理器架構為 64 位元,且 `unsigned long` 為 8 位元組寬度 (符合 LP64 資料模型),以下是可能的 `GENMASK` 巨集的定義: ```c #define GENMASK(h, l) \ (((~0UL) >> (LEFT)) & ((~0UL) >> (l) << (RIGHT))) ``` 請補完,使其行為符合預期。 #### 運作原理與解題 `AND` 的左邊可以看出是要透過 `~0UL` 右移產生低位的 `h` bits 均為 1 的 `unsigned long`,而右側則是以左移產生低位 `l` bits 均為 0 的 `unsigned long` 接著就可以用 `AND` 運算得到 `h` 至 `l` 之間的 bits 均為 1 的 bitmask。 因此 `LEFT` 應為 `63 - h`,以產生低位 `h` bits 均為 1 的 `unsigned long`, `RIGHT` 應為 `l` 以產生低位 `l` bits 均為 0 的 `unsigned long`。 > 由於左移超過變數長度時,其結果在 c 屬於未定義行為,因此先左移 `l` bits 再右移 `l` bits 以避免此情形發生。 :::warning TODO - 比較 Linux 核心 `GENMASK` 巨集的實作,闡述其額外的考量 - 舉出 Linux 核心原始程式碼中二處 `GENMASK` 巨集和 [include/linux/bitfield.h](https://github.com/torvalds/linux/blob/master/include/linux/bitfield.h) 的應用案例 ::: #### 比較 Linux 核心 `GENMASK` 巨集的實作,闡述其額外的考量 #### 舉出 Linux 核心原始程式碼中二處 `GENMASK` 巨集和 [include/linux/bitfield.h](https://github.com/torvalds/linux/blob/master/include/linux/bitfield.h) 的應用案例 ## 測驗 2 **題目** 考慮以下程式碼: ```c struct foo; struct fd { struct foo *foo; unsigned int flags; }; enum { FOO_DEFAULT = 0, FOO_ACTION, FOO_UNLOCK, } FOO_FLAGS; static inline struct fd to_fd(unsigned long v) { return (struct fd){EXP1, v & 3}; } ``` 函式 `fo_fd` 接受指定的地址 `v`,填入一個 `fd` 結構體,並確保第一個成員的地址得以依據〈[你所不知道的 C 語言:記憶體管理、對齊及硬體特性](https://hackmd.io/@sysprog/c-memory)〉描述,對 4 個位元組進行向下對齊 (即 C++ Boost 描述的 [align_down](https://www.boost.org/doc/libs/1_78_0/doc/html/align/reference.html#align.reference.functions.align_down))。其中 `struct foo;` 運用〈[你所不知道的 C 語言:指標篇](https://hackmd.io/@sysprog/c-pointer)〉提及的 forward declaration 技巧,可在實作階段才提供具體 `struct foo` 的定義。 請補完,使其行為符合預期。 #### 運作原理與解題 由於 `fd` 結構體的第一個成員是指向 `struct foo` 結構體的指標,因此 `EXP1` 肯定要將 `unsigned long` 型態的 `v` casting 成指向 `struct foo` 型態的指標,即 `EXP1` 前面一定包含 `(struct foo *)`。 接著由於要作 4 byte 的 align down 因此代表 2^1^, 2^0^ 的最低位 2 bits 應設為 0 (低位的 log~2~(4) 個 bits 設為 0),而這可以透過 `AND` 一個最低 2 位為 0 的 bitmask 達成,最簡潔的寫法是透過取 `3` 的 1 補數達成。 因此 `EXP1` = `(struct foo *)(v & ~3)` :::warning TODO - 在 Linux 核心原始程式碼找出類似技巧的實作 (例如檔案系統) 並解說 ::: #### 在 Linux 核心原始程式碼找出類似技巧的實作 (例如檔案系統) 並解說 ## 測驗 3 **題目** 考慮以下程式碼,能將輸入的 8 位元無號整數逐一位元反轉,如同 [LeetCode 190. Reverse Bits](https://leetcode.com/problems/reverse-bits/) 的描述。 ```c= #include <stdint.h> uint8_t rev8(uint8_t x) { x = (x >> 4) | (x << 4); x = ((x & 0xCC) >> 2) | (EXP2); x = ((x & 0xAA) >> 1) | (EXP3); return x; } ``` 請補完,使其行為符合預期。 #### 運作原理與解題 進行 `rev8` 前的一個 `uint8_t` 示意圖 ```graphviz digraph "reverse-bits" { node [shape=record]; rankdir=LR; a [label="{ 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 }"]; } ``` > 為求方便辨識原本的 0 到 7 bit 分別以 0 到 7 標注 #2 將高位 4 bits 與低位 4 bits 對調後 ```graphviz digraph "reverse-bits" { node [shape=record]; rankdir=LR; a [label="{ 3 | 2 | 1 | 0 | 7 | 6 | 5 | 4 }"]; } ``` #3 左半邊可以看出是將 4 bits 內的高位 2 bits 調至低位 2 bits,因此不難猜出 `EXP2` 是將 4 bits 內的低位 2 bits 調至高位 2 bits,調換後的示意圖如下 ```graphviz digraph "reverse-bits" { node [shape=record]; rankdir=LR; a [label="{ 1 | 0 | 3 | 2 | 5 | 4 | 7 | 6 }"]; } ``` #4 的左半邊當然就是將 2 bits 內的高位 bit 調至低位 bit,因此 `EXP3` 是將 2 bits 內的低位 bit 調至高位 bit,調換後的示意圖如下 ```graphviz digraph "reverse-bits" { node [shape=record]; rankdir=LR; a [label="{ 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 }"]; } ``` 由上面的步驟得出答案為 - `EXP2` = `(x & 0x33) << 2` - `EXP3` = `(x & 0x55) << 1` > `0x33` = `0b00110011`, `0x55` = `0b01010101` :::warning TODO - 在 Linux 核心原始程式碼找出類似技巧的實作並解說其應用場景 ::: #### 在 Linux 核心原始程式碼找出類似技巧的實作並解說其應用場景 ## 測驗 4 **題目** 延伸〈[你所不知道的 C 語言:前置處理器應用篇](https://hackmd.io/@sysprog/c-preprocessor)〉,我們嘗試將 [foreach 巨集](https://en.wikipedia.org/wiki/Foreach_loop) 予以擴充,使得支援以下寫法: ```c int i; foreach_int(i, 0, -1, 100, 0, -99) { printf("i is %i\n", i); } const char *p; foreach_ptr(p, "Hello", "world") { printf("p is %s\n", p); } ``` 預期輸出如下: ``` i is 0 i is -1 i is 100 i is 0 i is -99 p is Hello p is world ``` 對應的巨集定義: ```c #include <assert.h> #define _foreach_no_nullval(i, p, arr) \ assert((i) >= sizeof(arr) / sizeof(arr[0]) || (p)) #define foreach_int(i, ...) \ for (unsigned _foreach_i = (((i) = ((int[]){__VA_ARGS__})[0]), 0); \ _foreach_i < sizeof((int[]){__VA_ARGS__}) / sizeof(int); \ (i) = ((int[]){__VA_ARGS__, 0})[EXP4]) #define foreach_ptr(i, ...) \ for (unsigned _foreach_i = \ (((i) = (void *) ((typeof(i)[]){__VA_ARGS__})[0]), 0); \ (i); (i) = (void *) ((typeof(i)[]){__VA_ARGS__, \ NULL})[EXP5], \ _foreach_no_nullval(_foreach_i, i, \ ((const void *[]){__VA_ARGS__}))) ``` 請補完,使其行為符合預期。 #### 運作原理與解題 從程式碼可以看出不論是 `foreach_int`, `foreach_ptr` 中的 `_foreach_i` 都是用來標示讀到第幾個 `__VA_ARGS__` 的,因此每次迴圈 `foreach_int` 應加 1,而由於每次迴圈的更新要對 `i` 賦值,使其值等於下個 `__VA_ARGS__` 中的值,因此 `EXP4`, `EXP5` = `++_foreach_i`,而非 `_foreach_i++`。 (先更新 `foreach_int` 再對 `i` 賦值) :::warning TODO - 在 Linux 核心原始程式碼找出類似技巧的實作並解說其應用場景 ::: #### 在 Linux 核心原始程式碼找出類似技巧的實作並解說其應用場景 ## 測驗 5 **題目** 針對 [LeetCode 29. Divide Two Integers](https://leetcode.com/problems/divide-two-integers/),以下是可能的實作: ```c= #include <limits.h> int divide(int dividend, int divisor) { int signal = 1; unsigned int dvd = dividend; if (dividend < 0) { signal *= -1; dvd = ~dvd + 1; } unsigned int dvs = divisor; if (divisor < 0) { signal *= -1; dvs = ~dvs + 1; } int shift = 0; while (dvd > (EXP6)) shift++; unsigned int res = 0; while (dvd >= dvs) { while (dvd < (dvs << shift)) shift--; res |= (unsigned int) 1 << shift; EXP7; } if (signal == 1 && res >= INT_MAX) return INT_MAX; return res * signal; } ``` 請補完,使其行為符合預期。 #### 運作原理與解題 #5 ~ #15 用於處理當除數或被除數有負值的時候將其轉為正值後存入對應的 `unsigned int`,並將其正負號紀錄於 `signal` 變數,方便後續運算。 #18 則是當被除數大於除數乘上 2^shift^ 時,代表商的最高位的 1 (first set bit) 至少在第 `shift` bit。 就像十進位除法 900 除 10 我們會移到百位數後發現 9 < 10 最後從十位數開始寫商,而更大的位數則留下 0。 > 實際上跳出迴圈時 `shift` 會比商的 first set bit 的 index 多 1 但會在後續的 while loop handle 因此 `EXP6` = `dvs << shift` #22 開始的 while loop 是整個除法的主體,內層的 while loop 就是在處理先前比喻的 9 < 10 的情形下商會退 1 位再繼續寫若退 1 位後還是被除數還是小於位移後的除數就退到不小於為止。 當被除數不小於位移後的除數時就將 1 紀錄到 `res` 中對應的 bit,並將被除數減去位移後的除數的值,因此 `EXP7` = `dvd -= dvs << shift`,而 while loop 會進行到剩下的 `dvd` 小於 `dvs` 為止。 最後將 `res` 乘上先前紀錄的正負號 `signal` 即為答案。 :::warning TODO - 指出上述程式碼可改進之處,並予以實作 - 在 Linux 核心原始程式碼中找出針對整數/浮點數/定點數除法特別撰寫的程式碼,並予以討論 ::: #### 指出上述程式碼可改進之處,並予以實作 #### 在 Linux 核心原始程式碼中找出針對整數/浮點數/定點數除法特別撰寫的程式碼,並予以討論 ## 測驗 6 **題目** 針對 [LeetCode 149. Max Points on a Line](https://leetcode.com/problems/max-points-on-a-line/),以下是可能的實作: ```c #include <stdbool.h> #include "list.h" struct Point { int x, y; }; struct point_node { int p1, p2; struct list_head link; }; static bool can_insert(struct list_head *head, int p1, int p2) { struct point_node *pn; list_for_each_entry (pn, head, link) return EXP8; return true; } static int gcd(int x, int y) { while (y) { int tmp = y; y = x % y; x = tmp; } return x; } static int maxPoints(struct Point *points, int pointsSize) { if (pointsSize <= 2) return pointsSize; int i, j, slope_size = pointsSize * pointsSize / 2 + 133; int *dup_cnts = malloc(pointsSize * sizeof(int)); int *hori_cnts = malloc(pointsSize * sizeof(int)); int *vert_cnts = malloc(pointsSize * sizeof(int)); int *slope_cnts = malloc(slope_size * sizeof(int)); memset(hori_cnts, 0, pointsSize * sizeof(int)); memset(vert_cnts, 0, pointsSize * sizeof(int)); memset(slope_cnts, 0, slope_size * sizeof(int)); for (i = 0; i < pointsSize; i++) dup_cnts[i] = 1; struct list_head *heads = malloc(slope_size * sizeof(*heads)); for (i = 0; i < slope_size; i++) INIT_LIST_HEAD(&heads[i]); for (i = 0; i < pointsSize; i++) { for (j = i + 1; j < pointsSize; j++) { if (points[i].x == points[j].x) hori_cnts[i]++, hori_cnts[j]++; if (points[i].y == points[j].y) vert_cnts[i]++, vert_cnts[j]++; if (points[i].x == points[j].x && points[i].y == points[j].y) dup_cnts[i]++, dup_cnts[j]++; if (points[i].x != points[j].x && points[i].y != points[j].y) { int dx = points[j].x - points[i].x; int dy = points[j].y - points[i].y; int tmp = gcd(dx, dy); dx /= tmp; dy /= tmp; int hash = dx * dy - 1333 * (dx + dy); if (hash < 0) hash = -hash; hash %= slope_size; if (can_insert(&heads[hash], i, j)) { struct point_node *pn = malloc(sizeof(*pn)); pn->p1 = i; pn->p2 = j; EXP9; } } } } for (i = 0; i < slope_size; i++) { int index = -1; struct point_node *pn; list_for_each_entry (pn, &heads[i], link) { index = pn->p1; slope_cnts[i]++; } if (index >= 0) slope_cnts[i] += dup_cnts[index]; } int max_num = 0; for (i = 0; i < pointsSize; i++) { if (hori_cnts[i] + 1 > max_num) max_num = hori_cnts[i] + 1; if (vert_cnts[i] + 1 > max_num) max_num = vert_cnts[i] + 1; } for (i = 0; i < slope_size; i++) { if (slope_cnts[i] > max_num) max_num = slope_cnts[i]; } return max_num; } ``` 請補完,使其行為符合預期。 #### 運作原理與解題 :::warning TODO - 指出上述程式碼可改進之處,並予以實作 - 擴充 LeetCode 題目,考慮更多座標點的輸入 (例如超過 10 萬個) 時,設計有效的資料結構以降低記憶體開銷,並確保快速的執行 ::: #### 指出上述程式碼可改進之處,並予以實作 #### 擴充 LeetCode 題目,考慮更多座標點的輸入 (例如超過 10 萬個) 時,設計有效的資料結構以降低記憶體開銷,並確保快速的執行 ## 測驗 7 **題目** 考慮 `ilog32` 函式可針對給定的 32 位元無號數,計算扣除開頭的 `0`,最少需要多少位元才可儲存 (the minimum number of bits required to store an unsigned 32-bit value without any leading zero bits),以下是一個可能的實作: ```c= int ilog32(uint32_t v) { int ret = v > 0; int m = (v > 0xFFFFU) << 4; v >>= m; ret |= m; m = (v > 0xFFU) << 3; v >>= m; ret |= m; m = (v > 0xFU) << 2; v >>= m; ret |= m; m = EXP10; v >>= m; ret |= m; EXP11; return ret; } ``` 請補完,使其行為符合預期。 #### 運作原理與解題 #3 當 `v` 是非零的值就至少需要 1 bit 紀錄,故 `ret = v > 0` #4 ~ #6 當 `v` 的 first set 在高位的 16 bits 中, `v` 會右移 16 bits 並在剩下的 16 bits 繼續找 first set , `ret` 則加上 16。 若不是的話, `m` 會為 0 不影響後續步驟。 (會變成找低位的 16 bits 中使否有 first set) #7 ~ #9 則是找在剩下的 16 bits 中 `v` 的 first set 是否在較高的 8 bits 中,若有 `v` 會再右移 8 bits 並在剩下的 8 bits 繼續找 first set , `ret` 則加上 8。 後面依此類推,`EXP10` 是在最後 4 bits 中找 `v` 的 first set 是否在較高的 2 bits 中,因此 `EXP10` = `(v > 3) << 1`。 `EXP11` 是在最後 2 bits 中找 `v` 的 first set 是在較高的 bit 中或是較低的那個,若是在較高的就將 `ret += 1` 若不是就不用加,因此 `EXP11` = `ret += v > 1`。 > `EXP11` 已經是最後一步了,所以直接改 `ret` 就好,不須再對 `v` 做位移或先紀錄到 `m` 。 :::warning TODO - 在 Linux 核心原始程式碼找出類似實作並解說其應用場景 - 研讀論文《[Using de Bruijn Sequences to Index a 1 in a Computer Word](http://supertech.csail.mit.edu/papers/debruijn.pdf)》,探討缺乏硬體 ctz/clz 指令的微處理器上,如何實作 branchless 的 `ilog` - 運用〈[你所不知道的 C 語言:前置處理器應用篇](https://hackmd.io/@sysprog/c-preprocessor)〉和〈[你所不知道的 C 語言:數值系統篇](https://hackmd.io/@sysprog/c-numerics)〉提及的技巧,實作編譯時期 (compile-time) 的 `ilog2` 實作 ::: #### 在 Linux 核心原始程式碼找出類似實作並解說其應用場景 #### 研讀論文《[Using de Bruijn Sequences to Index a 1 in a Computer Word](http://supertech.csail.mit.edu/papers/debruijn.pdf)》,探討缺乏硬體 ctz/clz 指令的微處理器上,如何實作 branchless 的 `ilog` #### 運用〈[你所不知道的 C 語言:前置處理器應用篇](https://hackmd.io/@sysprog/c-preprocessor)〉和〈[你所不知道的 C 語言:數值系統篇](https://hackmd.io/@sysprog/c-numerics)〉提及的技巧,實作編譯時期 (compile-time) 的 `ilog2` 實作 ## 測驗 8 **題目** 考慮以下 C++ 二元搜尋樹的程式: ```cpp typedef struct tnode *tree; struct tnode { int data; tnode *left; tnode *right; tnode(int d) { data = d; left = right = 0; } }; void remove_data(tree &t, int d) { tnode *p = t; tnode *q; while (p != 0 && p->data != d) { if (d < p->data) { q = p; p = p->left; } else { q = p; p = p->right; } } if (p != 0) { if (p->left == 0) if (p == t) t = p->right; else if (p == q->left) q->left = p->right; else q->right = p->right; else if (p->right == 0) if (p == t) t = p->left; else if (p == q->left) q->left = p->left; else q->right = p->left; else { tnode *r = p->right; while (r->left) { q = r; r = r->left; } p->data = r->data; if (r == p->right) p->right = r->right; else q->left = r->right; p = r; } delete p; } } ``` 函式 `remove_data` 嘗試將指定的資料,自給定的二元搜尋樹移除,但原本的 `remove_data` 過於冗長,於是我們可善用 indirect pointer 改寫為以下: ```cpp void remove_data(tree &t, int d) { tnode **p = &t; while (*p != 0 && (*p)->data != d) { if (d < (*p)->data) EXP12; else EXP13; } tnode *q = *p; if (!q) return; if (!q->left) *p = q->right; else if (!q->right) *p = q->left; else { tnode **r = &q->right; while ((*r)->left) r = EXP14; q->data = (*r)->data; q = *r; *r = q->right; } delete q; } ``` 請補完,使其行為符合預期。 #### 運作原理與解題 前面的 while loop 是在找要刪除的 node 依據二元搜尋樹的特性當要刪除的 node `d` 小於當前的 node 的 data,應向 left child 繼續搜尋,大於時則向 right child 搜尋。 因此 `EXP12` = `p = &(*p)->left`, `EXP13` = `p = &(*p)->right`。 > 將 indirect pointer `p` 指向 `*p` 的 left 或 right 所在的記憶體位置 當要移除的 node 存在時 `*p` 會指向該 node,若否則 `*p` 會等於 `null`,而 `p` 則是指向該 node 的 parent 的 left 或 right 所在的記憶體位置。 因此接下來只須將 `*p` 指向接替 node `d` 的 node 就完成 remove 了。 **沒有左子樹或右子樹時** ```cpp if (!q->left) *p = q->right; else if (!q->right) *p = q->left; ``` 當被移除的 node 沒有左子樹時直接以右子樹取代該 node。 反之若沒有右子樹時直接以左子樹取代該 node。 **左右子樹都存在時** ```cpp tnode **r = &q->right; while ((*r)->left) r = EXP14; q->data = (*r)->data; q = *r; *r = q->right; ``` 這邊是從右子樹中找最小的 node 取代要移除的 node。 先以 `r` 指向要移除的 node 的 right,當該 node 有左子葉時 `r` 就指向 `(*r)->left` 所在的記憶體位置,一路找到沒有左子葉時就是右子樹中最小的 node。 接著將這個 node 的 data 複製到要刪除的 node 中取代要被刪除的值,並將 `q` 指向該 node,並將 `*r` 指向 `q` 的右子樹。 因此 `EXP14` = `&(*r)->left`。 最後刪除 `q` 即可。 :::warning TODO - 以 C99 改寫上述程式碼,並寫出更精簡高效的實作,和最初的實作相比,探討效率的提升 - 研讀 [Static B-Trees](https://en.algorithmica.org/hpc/data-structures/s-tree/),探討針對記憶體佈局 (memory layout) 的改進策略 ::: #### 以 C99 改寫上述程式碼,並寫出更精簡高效的實作,和最初的實作相比,探討效率的提升 #### 研讀 [Static B-Trees](https://en.algorithmica.org/hpc/data-structures/s-tree/),探討針對記憶體佈局 (memory layout) 的改進策略