# 2024q1 Homework2 (quiz1+2) contributed by < `eleanorLYJ` > > [作業規範](https://hackmd.io/@sysprog/BkmmEQCn6) ## [第一週測驗題](https://hackmd.io/@sysprog/linux2024-quiz1) ### 測驗1 #### 理解程式碼 有變數i,去模擬遞迴的stack的層數,用`begin[i]` 與 `end[i]` 代表 第i層考慮的左閉右閉的區間。每次把區間的第一個節點當pivot,接著從pivot->next 開始迭代向next走,直到把連 R 也走完 也就會把除了pivot的所有節點與pivot的值做比較或是說分類,節點大於pivot的加到right的開頭,反之與接到 left 的開頭。將所有節點分類完畢。 測驗寫的演算法,都是先考慮遞迴pivot左邊區間,之後將pivot存下,之後再遞迴pivot的右邊區間。 <!-- max_len是最大的遞迴層數 --> #### 提出改進方案並予以實作 - 使用List API改寫 - 提出可避免最差狀況的快速排序實作 想要解決: 與pivot同樣的值的節點仍要繼續遞迴。 <!-- #### 疑問與相關實驗 --> ### 測驗2 #### 閱讀題目時 Timsort 筆記 - timsort 核心想法: - 走訪過程把有序數列塞進 run, 適當時機就合併 - 關於 run - merge_collapse 確保 run 長度 - run 太短 就用二分插入 (插到其他的run裡) - 有個 stack 負責暫存 run, 前三個 run 稱為A, B, C - 關於合併 - 不考慮merge的兩個條件 1. `len(A) > len(B) + len(C)` 2. `len(B) > len(C)` -> 其一不符合就考慮合併 - 兩種合併排序 (如果len(A) < len(B) 就會把A 暫存起來) 1. one-pair-at-a-time (經典排序) 2. collpase mode: (考慮兩個已排序的數列時): 找 B[0] 應該插在 A 的哪位置,接著 B[1] 考慮 B[0] 以右的位置,直到把 B 陣列的值全部都插入到 A 陣列中 #### 理解程式碼 原本以為`run_size`函數寫錯,發現是把size 存在 `節點->next->pre`裡。 #### 提出改進方案並予以實作 閱讀程式碼後發現沒有作merge galloping, 我原本著手撰寫merge galloping, 但多加思考後是此演算法要實作在串列上, 然而普通合併方式與 merge galloping 效果會相同, 因此程式碼就維持不變 #### 資料分布 設計效能評比的測試程式與 Timsort 設想的資料分布樣式。 我測試資料設計成6種類別,分別為(0)全部隨機資料、(1)遞增資料、(2)遞減資料、(3)遞增資料並隨機交換3次、(4)遞增資料隨機交換7次、(5)每64個數字作為一個 run 組成的資料、(6)每64個數字作為一個 run 組成的資料並在同一個 run 以20%機率隨機交換 (7)全部資料以同一個隨機值組成 [github](https://github.com/eleanorLYJ/lab2) ``` ==== Testing timsort ==== type == 0 Elapsed time: 4507 Comparisons: 482810 List is sorted type == 1 Elapsed time: 63 Comparisons: 13357 List is sorted type == 2 Elapsed time: 54 Comparisons: 13357 List is sorted type == 3 Elapsed time: 0 Comparisons: 0 List is sorted type == 4 Elapsed time: 0 Comparisons: 0 List is sorted type == 5 Elapsed time: 0 Comparisons: 0 List is sorted type == 6 Elapsed time: 0 Comparisons: 0 List is sorted type == 7 Elapsed time: 1 Comparisons: 0 List is sorted ==== Testing list_sort ==== type == 0 Elapsed time: 3310 Comparisons: 450213 List is sorted type == 1 Elapsed time: 306 Comparisons: 93373 List is sorted type == 2 Elapsed time: 269 Comparisons: 90613 List is sorted type == 3 Elapsed time: 2 Comparisons: 0 List is sorted type == 4 Elapsed time: 2 Comparisons: 0 List is sorted type == 5 Elapsed time: 1 Comparisons: 0 List is sorted type == 6 Elapsed time: 0 Comparisons: 0 List is sorted type == 7 Elapsed time: 0 Comparisons: 0 List is sorted ``` TODO: 1. 將以上程式碼合併進lab0 , 作為一個有效的排序命令 2. 確定亂數足夠亂 ## [第二周測驗題](https://hackmd.io/@sysprog/linux2024-quiz2) ### 測驗 1 > 前序可知哪個節點在上面 (越前面的越上面)、而由中序可知哪個節點靠左(越前面的越左邊) #### 理解程式碼 觀察 `dfs`函式如何遞迴的,其中著重理解參數意義。 ```c static struct TreeNode *dfs(int *preorder, int pre_low, int pre_high, int *inorder, int in_low, int in_high, struct hlist_head *in_heads, int size) //省略終止條件與建 `TreeNode *tn` 的過程 tn->left = dfs(preorder, pre_low + 1, pre_low + (idx - in_low), inorder, in_low, idx - 1, in_heads, size); tn->right = dfs(preorder, pre_high - (in_high - idx - 1), pre_high, inorder, idx + 1, in_high, in_heads, size); ``` 首先知道,變數`idx` 是當前節點(`tn`)在中序陣列中節點的索引值。 `dfs` 函式中的參數 `in_low` 與 `in_high` 比較好推測意義。觀察 `tn->left`, 遞迴給`tn`的左子樹,範圍 `in_low` 到 `idx-1` (含)的中序索引範圍,而右子樹則是 `idx+1` (含) 到 `in_hight` 以後的索引範圍。 也就是用 `idx` 分左右子樹的範圍. 接著觀察 `tn->left`,遞迴給 `tn` 左子樹中的`pre_low` 的值是 `pre_low + 1` ,因為建節點順序是按前序的順序,而當前節點`tn`已建值為`preorder[pre_low]` 的節點,因此 `tn` 的左子節點的值就是 `preorder[pre_low+1]`。而`tn`遞迴給左子樹的`pre_high` 是 `pre_low + (idx - in_low)`,`idx - in_low`是從中序觀點看目前 `tn` 的左子樹的節點個數。因此從`pre_low` 加左子樹的節點個數就是 `tn` 的左子樹要考慮的前序範圍。 我的結論: `pre_low`、`pre_high`、`in_low` 和 `in_high` 參數是用於控制遞迴的範圍 - `pre_low` 與 `pre_high` 代表當前遞迴要考慮的前序索引範圍(閉區間)。 - `in_low` 與 `in_high` 代表當前遞迴要考慮的中序索引範圍(閉區間)。 <!-- #### 可改進之處與實作 --> ### linux核心 pre-order walk程式碼探討 > 在 Linux 核心的 cgroups 相關原始程式碼,找出 pre-order walk 程式碼並探討 kernel/cgroup/cgroup.c: * css_next_descendant_pre - find the next descendant for pre-order walk ```c struct cgroup_subsys_state * css_next_descendant_pre(struct cgroup_subsys_state *pos, struct cgroup_subsys_state *root) { struct cgroup_subsys_state *next; cgroup_assert_mutex_or_rcu_locked(); /* if first iteration, visit @root */ if (!pos) return root; /* visit the first child if exists */ next = css_next_child(NULL, pos); if (next) return next; /* no child, visit my or the closest ancestor's next sibling */ while (pos != root) { next = css_next_child(pos, pos->parent); if (next) return next; pos = pos->parent; } return NULL; } EXPORT_SYMBOL_GPL(css_next_descendant_pre); ``` ### 測驗 2 #### 理解程式碼 <!-- #### 改進與實作 撰寫完整的測試程式,指出其中可改進之處並實作 #### 討論 LRU 相關程式碼 在 Linux 核心找出 LRU 相關程式碼並探討 --> ### 測驗 3 #### 理解程式碼 `find_nth_bit` 指定的記憶體空間找出第 N 個設定的位元 ##### `__ffs` `__ffs` 找在word中第一個 bit (find first bit in word) 做法為逐步檢查數字中每個 2 的冪位元組,若該範圍內皆為 0,則將該範圍的位元數累加至 num 變數中,最後返回 num。若某個範圍內有 1 出現,則停止檢查並返回 num <!-- 其中思考為什麼要判斷 BIT_PER_LONG 是否為64 bit,因為不同的作業系統不同長度。 --> <!-- [wikipedia: 64-bit computing](https://en.wikipedia.org/wiki/64-bit_computing#64-bit_data_models) --> <!-- ![image](https://hackmd.io/_uploads/r1wdYS410.png) --> ```c static inline unsigned long __ffs(unsigned long word) { int num = 0; #if BITS_PER_LONG == 64 if ((word & AAAA) == 0) { num += 32; word >>= 32; } #endif if ((word & 0xffff) == 0) { num += 16; word >>= 16; } if ((word & 0xff) == 0) { num += 8; word >>= 8; } if ((word & 0xf) == 0) { num += 4; word >>= 4; } if ((word & 0x3) == 0) { num += 2; word >>= 2; } if ((word & 0x1) == 0) num += 1; return num; } ``` 帶入值確定做法。 (二進位的表示,我以每2的冪作區隔) 如果 64 位元全0 `__ffs` 就會得到 63 word = 0|00|0000|00000000|0000000000000000|00000000000000000000000000000000|0000000000000000000000000000000000000000000000000000000000000000 num = 32+16+8+4+2+1 = 63 若 32 位元中的第一個位元為 1,`__ffs` 將返回 30 word = 1|00|0000|00000000|0000000000000000|00000000000000000000000000000000 -> num = 16+8+4+2=30 若 32 位元中的第 1 和 2 位元為 1,`__ffs` 將返回 28 word = 1|10|0000|00000000|0000000000000000|00000000000000000000000000000000 ->num = 16+8+4+0 = 28 能看出`ffs`得到從右數數過來第一個1是出現在第 n 個位元為 ##### `__clear_bit` ```c static inline void __clear_bit(unsigned long nr, volatile unsigned long *addr) { unsigned long mask = BIT_MASK(nr); unsigned long *p = ((unsigned long *) addr) + BIT_WORD(nr); *p &= BBBB; } ``` `#define BIT_MASK(nr) (1UL << ((nr) % BITS_PER_LONG)) ` BIT_MASK 生成出遮罩,只有第(nr) % BITS_PER_LONG bit 為1 ,其餘為0的遮罩。其做法為將1往左位移 ((nr) % BITS_PER_LONG)) 位 `#define BIT_WORD(nr) ((nr) / BITS_PER_LONG)` 用來計算指定位元 nr 在位址的 word 索引。其做法為超過 `BITS_PER_LONG` 就回傳1 ,反之,小於BITS_PER_LONG就回傳0。 `clear_bit` 先取得遮罩,再判斷 `nr` 長度若超過`BITS_PER_LONG`,`addr` 加一,得到指向`nr` 所在的位元組的指標 `p`。接著將遮罩做位元翻轉(`~mask`),再與 `*p` 做 AND 運作,將 位元`nr` 設為0。 ##### 計算一個數字中設置為 1 的位元數量 ```c #define __const_hweight8(w) \ ((unsigned int) ((!!((w) & (1ULL << 0))) + (!!((w) & (1ULL << 1))) + \ (!!((w) & (1ULL << 2))) + (!!((w) & (1ULL << 3))) + \ (!!((w) & (1ULL << 4))) + (!!((w) & (1ULL << 5))) + \ (!!((w) & (1ULL << 6))) + (!!((w) & (1ULL << 7))))) #define __const_hweight16(w) (__const_hweight8(w) + __const_hweight8((w) >> 8)) #define __const_hweight32(w) \ (__const_hweight16(w) + __const_hweight16((w) >> 16)) #define __const_hweight64(w) \ (__const_hweight32(w) + __const_hweight32((w) >> 32)) static inline unsigned long hweight_long(unsigned long w) { return __const_hweight64(w); } ``` 這段程式碼定義了計算一個數字中設置為 1 的位元數量的巨集和函式。 `__const_hweight8` 計算一個 8 位元數字中設置為 1 的位元數量。這是通過對數字進行 8 次位元 AND 運算,每次都檢查該位元是否設置為 1,如果是則將結果加 1。最後,返回加總的結果。 這巨集裡的`!!` 能確保其結果為 0 或 1。 接著,`__const_hweight16` 使用 `__const_hweight8` 函式計算給定數字的前 8 位元和後 8 位元的位元數量,並將結果相加。 `__const_hweight32` 和 `__const_hweight64` 類似地使用 `__const_hweight16` 函式來計算給定數字的前 16 位元、前 32 位元和前 64 位元的位元數量,並將結果相加。 ##### `fns` `fns` : 找第n個被設為1的位元位置 (find N'th set bit in a word) while 迴圈內,變數 `bit`的值 為 `__ffs` 函式找到 word 中最低位設為 1 的位元的位置,之後變 n 減一,接著使用 `__clear_bit`清除`bit`所在的位元,也是讓下一次迭代找到的最小位元是此次迭代的次小位元。然而當 n 為 0 就回傳當前最小位元。倘若n 大於 word 全部有被設為1的位元總量 或是 word 就是 0,兩情況就返回 `BITS_PER_LONG`。 ```c // @word: The word to search // @n: Bit to find static inline unsigned long fns(unsigned long word, unsigned int n) { while (word) { unsigned int bit = __ffs(word); if (n-- == 0) return bit; __clear_bit(bit, &word); } return BITS_PER_LONG; } ``` ##### ‵GENMASK` ```c #define GENMASK(h, l) \ (((~0UL) - (1UL << (l)) + 1) & (~0UL >> (BITS_PER_LONG - 1 - (h)))) ``` `((~0UL) - (1UL << (l)) + 1)`: 創造一個 l 之前都是 1,之後全是 0 的二進位 例如 當 l = 5 二進位 = 1111111111111111111111111111111111111111111111111111111111100000 `(~0UL >> (BITS_PER_LONG - 1 - (h))` : 創造一個第 `h+1` 之前位元都是 0,之後全是 1 的二進位。 例如 當 h = 5 二進位 = 00000000000000000000000000000000000000000000000000000000000111111 GENMASK 最終能得到介於 第 h 與 l 之間的位元為 1 其餘為 0 的遮罩。 ##### `BITMAP_LAST_WORD_MASK` ```c #define BITMAP_LAST_WORD_MASK(nbits) (~0UL >> (-(nbits) & (BITS_PER_LONG - 1))) ``` ~0UL 全部位元都設置為 1 的值。而 (-(nbits) & (BITS_PER_LONG - 1))決定要讓~0UL往右移多少。 首先我查 [C Operator Precedence](https://en.cppreference.com/w/c/language/operator_precedence)。為 unary minus的 - 會優先於 Bitwise AND,所以 `(-(nbits) & (BITS_PER_LONG - 1))` 先將 `nbits` 取二補數之後,再與 `BITS_PER_LONG - 1` 進行做 AND 運算。其中 `BITS_PER_LONG - 1`為常數63 (二進位為 11111),所以這個 AND 運算只會保留`-(nbits)`最後一個BITS_PER_LLONG長度的有效位元。 接著我帶入值,觀察此巨集效果 {nbits} -- {(nbits) & (BITS_PER_LONG - 1) (十進位)} -> {BITMAP_LAST_WORD_INDEX(nbits)} 0 -- 0(0) ->11111111111111111111111111111111 1 -- 111111(63) ->1 2 -- 111110(62) ->11 3 -- 111101(61) ->111 4 -- 111100(60) ->1111 5 -- 111011(59) ->11111 6 -- 111010(58) ->111111 7 -- 111001(57) ->1111111 8 -- 111000(56) ->11111111 9 -- 110111(55) ->111111111 設 n = -(nbits) 巨集會返回的值是 $2^{64-(n)} -1$ ##### small_const_nbits `__builtin_constant_p` 判斷其參數值是否在編譯時期就知道參數 ```c #define small_const_nbits(nbits) \ (__builtin_constant_p(nbits) && (nbits) <= BITS_PER_LONG && (nbits) > 0) ``` ##### FIND_NTH_BIT ```c #define FIND_NTH_BIT(FETCH, size, num) \ ({ \ unsigned long sz = (size), nr = (num), idx, w, tmp; \ \ for (idx = 0; (idx + 1) * BITS_PER_LONG <= sz; idx++) { \ if (idx * BITS_PER_LONG + nr >= sz) \ goto out; \ \ tmp = (FETCH); \ w = hweight_long(tmp); \ if (w > nr) \ goto found; \ \ nr -= w; \ } \ \ if (sz CCCC BITS_PER_LONG) \ tmp = (FETCH) & BITMAP_LAST_WORD_MASK(sz); \ found: \ sz = min(idx * BITS_PER_LONG + fns(tmp, nr), sz); \ out: \ sz; \ }) ``` ##### find_nth_bit >不理解: idx是? ```c static inline unsigned long find_nth_bit(const unsigned long *addr, unsigned long size, unsigned long n) { if (n >= size) return size; if (small_const_nbits(size)) { unsigned long val = *addr & GENMASK(size - 1, 0); return val ? fns(val, n) : size; } return FIND_NTH_BIT(addr[idx], size, n); } ``` <!-- #### 說明 Linux 核心中 find_nth_bit的應用 > Linux 核心原始程式碼找出 find_nth_bit 的應用案例,並予以解說和分析,需要涵蓋 CPU affinity 相關的原始程式碼 -->