--- tags: linux2022, linux --- # 2022q1 Homework3 (quiz3) contributed by < `AmyLin0210` > > [2022q1 第 3 週測驗題](https://hackmd.io/@sysprog/linux2022-quiz3) ---- ## 測驗 1 ### 解釋程式碼運作原理 ```c #define GENMASK(h, l) \ (((~0UL) >> (63 - h)) & ((~0UL) >> (l) << (l))) ``` 首先我們看到 `(~0UL) >> (63 - h)` 的部份,這裡代表將左側非一的位置清零, 在後面 `(~0UL) >> (l) << (l)` 則是將右側非一的位置清零, 兩者再做 **and** 的操作,便可以只留下中間需要為 1 的數字。 下方我以 8 bit 作為示意圖 ``` GENMASK(6, 4) 01110000 11111111 >> (7 - 6) --> 01111111 11111111 >> (4) << (4) --> 11110000 ------------------------------------ 01111111 & 11110000 --> 01110000 ``` ## 測驗 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){(struct foo *)(v & ~3), v & 3}; } ``` 由題目敘述可以得知,我們想要針對 `foo` 這個指標以四個位元組為單位,進行向下對齊。 在一般的想法上,就是直接將該位置除以四,但由於 4 是二的倍數,所以我們可以使用 binary operation 來達成,答案是 `(struct foo *)(v & ~3)` ## 測驗 3 ### 解釋程式碼運作原理 ```c #include <stdint.h> uint8_t rev8(uint8_t x) { x = (x >> 4) | (x << 4); x = ((x & 0xCC) >> 2) | ((x & 0x33) << 2); x = ((x & 0xAA) >> 1) | ((x & 0x55) << 1); return x; } ``` 以下是整個流程的示意圖: 每 4 個 bits 為一組,做 reverse ``` x = (x >> 4) | (x << 4) 1 2 3 4 | 5 6 7 8 -> 5 6 7 8 | 1 2 3 4 ``` 再來由於 0xCC 為 `1100 1100` 若有一個數字與它做 and 操作,並往右位移兩位,那便會有共 4 個位元,以兩個位元為一組,向右位移兩位。 類似的,由於 0x33 為 `0011 0011` ,若有數字與它做 and 操作,並往左位移兩位,那便會有共 4 個位元,以兩個位元為一組,向左位移兩位。 ``` x = ((x & 0xCC) >> 2) | ((x & 0x33) << 2) 5 6 | 7 8 | 1 2 | 3 4 -> 7 8 | 5 6 | 3 4 | 1 2 ``` 最後兩個兩個位元一組左右互換 ``` x = ((x & 0xAA) >> 1) | ((x & 0x55) << 1) 7 | 8 | 5 | 6 | 3 | 5 | 1 | 2 -> 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 ``` ### 測驗 4 首先,我們先來看 `__VA_ARGS__` 到底是什麼。 在 [Variadic Macros](https://gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html) 這份文件中,有解釋到他是一個可以被宣告為可變參數的 macro,以下方的程式碼為例 ```c= #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})[++_foreach_i]) int main () { int i; foreach_int(i, 0, -1, 100, 0, -99) { printf("i is %i\n", i); } } ``` 第 8 行的地方使用了上方定義的 foreach_int 巨集 (macro),`__VA_ARGS__` 將會是該巨集第二個之後的參數們,若是將它展開,會等同於下方的程式碼: ```c= for (unsigned _foreach_i = (((i) = ((int[]){0, -1, 100, 0, -99})[0]), 0); \ _foreach_i < sizeof((int[]){0, -1, 100, 0, -99}) / sizeof(int); \ (i) = ((int[]){0, -1, 100, 0, -99, 0})[++_foreach_i]) { printf("i is %i\n", i); } ``` - 一步一步的拆解上方程式碼,首先是 `(int[]){0, -1, 100, 0, -99})` ,這代表了一個陣列,裡面的元素分別為 `0, -1, 100, 0, -99` 。 - `((i) = ((int[]){0, -1, 100, 0, -99})[0])` 表示將該陣列第 0 個元素的值賦值給 `i` - 最後有運用到 [comma operator](https://www.geeksforgeeks.org/comna-in-c-and-c),也就是假設現在有以下的程式碼:`int i = (exp1, exp2)` ,程式執行時會先執行 exp1 再值執行 exp2 ,並回傳出 exp2 的結果給 `i`。在上面的程式碼,則代表把 `0` 這個數字賦值給 `_foreach_i`。 - 在這個 for 迴圈的中止條件是 `_foreach_i < sizeof((int[]){0, -1, 100, 0, -99}) / sizeof(int);`,所以推測 _foreach_i 儲存的資訊是已經遍歷到陣列的哪個位置 - 最後看到 `(i) = ((int[]){0, -1, 100, 0, -99, 0})[++_foreach_i])` ,表示要把 i 更新為新的陣列內位置的值,`++_foreach_i` 是先加一再回傳,因此可以拿到原陣列位置加一後的資料。 在 `foreach_ptr` 裡面的邏輯大致與 `foreach_int` 類似,同樣套用範例將它展開: ```c= #include <assert.h> #define _foreach_no_nullval(i, p, arr) \ assert((i) >= sizeof(arr) / sizeof(arr[0]) || (p)) const char *p; for (unsigned _foreach_i = \ (((i) = (void *) ((typeof(i)[]){"Hello", "world"})[0]), 0); \ (i); (i) = (void *) ((typeof(i)[]){"Hello", "world", \ NULL})[++_foreach_i], \ _foreach_no_nullval(_foreach_i, i, \ ((const void *[]){"Hello", "world"}))) ``` 我們可以看到在上面第 9 行的程式碼部份,在陣列的最後一個位置塞入 NULL 表示結束。 來看看 `_foreach_no_nullval` 這個巨集內是如何判斷該陣列正確。 - 在裡面的 `(i) >= sizeof(arr) / sizeof(arr[0])` ,由於 `||` 的特性,當 `(exp1 || exp2)` 時,只有在 exp1 為 `false` 的狀況下,才會去執行 `exp2` - 在這裡的話,`sizeof(arr) / sizeof(arr[0])` 算出的數字為該陣列內有幾個元素,當 i 還小於等於那個元素,我就去檢查 `p` 是否為 `NULL` ,若 `p` 為 `NULL` ,`assert` 內的判斷式為 `false` ,表示我會在 `stderr` 內輸出錯誤訊息並結束程式。 ## 測驗 5 ```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; dvd -= dvs << shift; } if (signal == 1 && res >= INT_MAX) return INT_MAX; return res * signal; } ``` 同樣一行行的來看程式碼, - 第 4 到第 15 行,做的事情都是去判斷 `dividend` 和 `divisor` 是否為負數,若是負數,則利用二補數的特性,將它轉為正數,並且在 signal 這個參數儲存他是正數或者負數的資訊。 - 在第 17 到 20 行,由於在二進制的 shift 等同於乘以二的冪次,所以就是找到 dvd 大於 dvs 的多少二的冪次。 - 第 22 到第 27 行,在 `dvd` 仍大於 `dvs` 的狀況下,相減他們,並使用 `res` 來記住已經減了多少,一次就是減 `dvs` 乘以二的冪次,使用 shift 來紀錄是要 shift 多少位。 ## 測驗 6 ```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 p1 == pn->p1; 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; list_add(&pn->link, &heads[hash]); } } } } 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; } ``` 在這份程式碼裡的想法是去分別計算對於一個點來說,與他處在相同 x 軸、y 軸、擁有相同斜率的點有哪些。 - 在第 63 到 79 行的地方,做的事情就是去找到相同斜率的點。 - 由於在電腦中,儲存浮點數很容易會有精度上的的問題,所以選擇算出兩點之間 x 軸與 y 軸之間的距離,再去找到他們的最大公因數,除以最大公因數後,以此紀錄他們的斜率。 - 在第 73 行呼叫了 `can_insert`,這個函式的內容定義在第 13 行,可以看到當中的做的事情是,只去記錄相同斜率狀況下,從同一個點出發的次數以避免重複呼叫。這個想法很類似於數學上的點斜式,也就是給定一個點,給定一個向量,就可以求出一個直線。 - 但是在目前的程式碼中,我發現可能會出現一個狀況,就是有機會非共線但相同斜率的狀態下,有些點會被捨棄掉。因此應該要重新設計演算法,將從哪些位置出發也列入考量。 ## 測驗 7 ```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 = (v > 0x3U) << 1; v >>= m; ret |= m; ret |= v > 0x1U; return ret; } ``` 在這裡,基本上是去找到最高位元是哪一個,以上的實作方式的優點是當中沒有 branch,若直觀的使用 for 迴圈去找出最高的位元在哪裡的話,就無可避免地會有 branch 的發生。 - 第 4 行的地方,去看有沒有超過 $2^{2^{4}}$ ,如果有就把結果儲存到 ret 裡,並向右 shift 相對應的位元。 - 第 10 行的地方,同上面的步驟,看看有沒有超過 $2^{2^{3}}$。 - 第 13 與第 16 行皆是相同的邏輯。 ## 測驗 8 在本測驗裡,主要是要將程式碼使用指標的指標來簡化。 ```c= void remove_data(tree &t, int d) { tnode **p = &t; while (*p != 0 && (*p)->data != d) { if (d < (*p)->data) p = &(*p)->left; else p = &(*p)->right; } 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 = &(*r)->left; q->data = (*r)->data; q = *r; *r = q->right; } delete q; } ``` 程式碼的第 4 到第 8 行的部分,是找到想要移除的結點在哪裡,若想移除的節點值較目前節點的小往左走,反之往右走。 在第 14 行,若找到的節點左邊已經沒有節點了,那直接將右邊的整個結構往上挪 假設我現在想要移除的 data 是 10 ,那我只需要把該節點替換為該節點的 right 即可 ``` 13 13 / \ / \ 10 15 11 15 \ --> \ 11 12 \ 12 ``` 在 16 行做的事情與上述的相似,差別只在於若現在是右邊沒有節點了,那我直接把左邊的結構網上挪。 第 18 到第 25 行處理的狀況為,若左右都有節點,那我先找到該節點右邊子樹中最小的 (也就是子樹中最左邊的 leaf),將他的值替換至想要被取代的節點。 第 24 行做的事情就是將該子樹最左邊的 leaf 的右子樹往上挪,以下圖範例來看,就是將 `16` 挪至原本 `15` 的位置 假設我們現在想要取代的的數字是 13 ``` 13 15 / \ / \ 10 18 10 18 / --> / 15 16 \ \ 16 17 \ 17 ``` ## 測驗 9 ```c /* maximum alignment needed for any type on this platform, rounded up to a power of two */ #define MAX_ALIGNMENT 16 /* Given a size, round up to the next multiple of sizeof(void *) */ #define ROUND_UP_TO_ALIGNMENT_SIZE(x) \ (((x) + MAX_ALIGNMENT - MMM) & ~(NNN)) ``` 首先我們可以看到以上的程式碼,和測驗 2 有些相似之處。 首先看到 `NNN` 的部分,因為我們會希望可以對齊 16,所以會想要把最後面的 4 個位元做 mask 處理掉,因此在這邊會是 `MAX_ALIGNMENT - 1` ,也就等同於 `0xFU`。 再來是 `MMM` 的部分。在題目裡,會希望可以做到向上對齊,但是如果直接將最後的四個位元給歸零,做的事情是向下對齊。因此我們可以將他們加上一個數字,向上位移至上一個位元組,再向下對齊。 在這裡的 `MMM` 是 `1` ,我們討論一下邊界的狀況,當原先的位置最後 4 個位元就是 0 時,對齊結果應該要是和原先相同的。在這裡要將 `x` 加上 `MAX_ALIGNMENT` 後再扣掉一就是考量這個原因。 ## 測驗 10 ```c #define DIVIDE_ROUND_CLOSEST(x, divisor) \ ({ \ typeof(x) __x = x; \ typeof(divisor) __d = divisor; \ (((typeof(x)) -1) > 0 || ((typeof(divisor)) -1) > 0 || \ (((__x) > 0) == ((__d) > 0))) \ ? ((RRR) / (__d)) \ : ((SSS) / (__d)); \ }) ``` 在這裡我參考了 [NOVBobLee](https://hackmd.io/@at0mCe11/linux2022-quiz3#%E6%B8%AC%E9%A9%97-10) 同學的筆記,當中有對於在不同型態下,進行除法運算時的不同結果。 除了在不同型態下的除法運算邏輯外,主要應用到的邏輯與測驗 9 的向上位移相似,所以 RRR 是 `__x + ((__d) >> 1)` 而 SSS 處理的為 x 是負數的狀況,因此是加減正好顛倒, `__x - ((__d) >> 1)` ## 測驗 11