# 2025q1 Homework2 (quiz1+2) > contributed by < `RealBigMickey` > # quiz1 ## 測驗一 **填空格 -** list_insert_before 函式: ```c list_item_t **p; for (p = &l; *p != before; p = &(*p)->next) ; *p = item; item->next = before; ``` ![graph](https://hackmd.io/_uploads/SkqHi-Whkl.png) 測驗一的程式碼目的是將 items 這個 array 中的所有元素插入一 list l 之中,先是插到 l.head 前,再檢查結束後的清單大小是否為N。 > */\* Test inserting at the beginning \*/* ![graph (1)](https://hackmd.io/_uploads/rJThxzb3ye.png) 再來是插在 NULL 前(清單最後),並檢查節點 value 是否正確。 > */\* Test inserting at the end \*/* ![graph (2)](https://hackmd.io/_uploads/BkcLbzW2kl.png) 最後一樣是插到最後,但插入結束後只會檢查清單大小是否為N。 --- ```c #define my_assert(test, message) \ do { \ if (!(test)) \ return message; \ } while (0) ``` 這邊使用 do-while 可使巨集變得好幾行,行為更貼近函式。 ### 為什麼使用巨集而不使用函式? my_run_test 與 my_assert 都使用於回傳 char* 的函式中,若使用函式的話就得多一行偵測它回傳的值,才能再決定是否 return。 :::info e.g. 使用 function : ```c const char* check_value(int x) { const char* error = my_assert(x > 0, "x must be positive"); if (error) return error; return "OK"; } ``` 使用 macro : ```c const char* check_value(int x) { my_assert(x > 0, "x must be positive"); return "OK"; } ``` ::: - 耗更多記憶體 - 速度更慢 - 多一行程式碼 ## 測驗二 我認為提供的程式碼試圖模擬 allocater 的工作,在控制的環境中模擬出配置記憶體與分頁等。 結構為樹狀、每個 block 都有兩個子節點,而函式 remove_free_tree 的目標是利用 Binary Search Tree 的概念去尋找 target 並釋放記憶體。 - 若沒有子節點 -> 直接刪除 - 若有一子節點 -> 子節點取代自己 - 若有兩子節點 -> 找出 in-order predecessor 來取代自己 ![graph (3)](https://hackmd.io/_uploads/B1iy7m-2yx.png) **填空:** ```c if ((*node_ptr)->l && (*node_ptr)->r) { /* Find the in-order predecessor: * This is the rightmost node in the left subtree. */ block_t **pred_ptr = &(*node_ptr)->l; while ((*pred_ptr)->r) pred_ptr = &(*pred_ptr)->r; ``` :::success 要求: - 解釋上方程式碼運作的原理,並嘗試補完程式碼,使其可獨立執行,應提供相關的測試程式碼 - 參閱 [memory-allocators](https://github.com/mtrebi/memory-allocators),針對你補完 (及後續改進) 的記憶體配置器,進行效能評比,並解讀其效能表現 ::: 補上缺的兩函式: ```c block_t **find_free_tree(block_t **root, block_t *target) { if (!root || !*root) return NULL; if (target->size < (*root)->size) { return find_free_tree(&(*root)->l, target); } else if (target->size > (*root)->size) { return find_free_tree(&(*root)->r, target); } else { if (*root == target) return root; block_t **returner = find_free_tree(&(*root)->l, target); if (!returner) returner = find_free_tree(&(*root)->r, target); return returner; } } block_t *find_predecessor_free_tree(block_t **root, block_t *node) { if (!root || !*root || !node->l) return NULL; block_t *cur = node->l; while (cur->r) cur = cur->r; return cur; } ``` 我認為若是要記憶體配置,則不太可能只有每個 size 指出現過一次。因此先用 BST 去找正確的位置,再從那邊出發找完全相符的目標元素。 **測試程式碼:** ```c int main() { block_t t_root = {50, NULL, NULL}; block_t n1 = {30, NULL, NULL}; block_t n2 = {70, NULL, NULL}; block_t n3 = {20, NULL, NULL}; block_t n4 = {40, NULL, NULL}; block_t n5 = {60, NULL, NULL}; block_t n6 = {80, NULL, NULL}; /* Manually constructing BST */ t_root.l = &n1; t_root.r = &n2; n1.l = &n3; n1.r = &n4; n2.l = &n5; n2.r = &n6; block_t *root = &t_root; printf("Before removing 30:\n"); print_tree(root, 0); remove_free_tree(&root, &n1); // Remove 30 printf("\nAfter removing 30:\n"); print_tree(root, 0); remove_free_tree(&root, &n3); // Remove 20 (leaf) printf("\nAfter removing 20:\n"); print_tree(root, 0); return 0; } ``` | 特性 | BST-分配器 | 傳統 malloc | | -------- | -------- | -------- | | 分配 | O(log n) | O(n)、 O(1)(快取) | | 釋放 | O(log n) | O(n) | | 碎片化處理 | 較好 | 較差| | 搜尋 free block | O(log n)(BST 搜尋) | O(n)(無快取) | | Metadata| 需要額外指標 & 樹平衡 | 簡單 linked list | ## 測驗三 在 linked list 中實現 quicksort ,因 linked list 不像是矩陣那樣,可自由抓每一部份,要操作資料就得走到節點處才行,所以用傳統的方式來 quicksort 效率上很差,而且又不希望使用遞迴,因此活用 stack 來完成快速排序。 運作原理: - 創兩個 stack ,一為 begin 左邊界 、一為 end 右邊界 - 從頭開始,pivot & L 為頭節點、R 為尾節點(寫一 helper function 來方便找尾節點) - 假設 cur 為目前節點,cur 開始走過每個節點,從 begin.pop 到 end.pop ,將所有小於 pivot 的點推入 left 的 sublist 中,而所有大於的丟入 right。再來在 begin 與 end 中新增新的節點 - 再來先處理 right sublist 再 left,重複操作。 :::success 要求: - 解釋上述程式碼運作原理 - 研讀[〈A comparative study of linked list sorting algorithms〉](),分析針對雙向鏈結串列的排序演算法考量,並運用 Linux 核心風格的 List API 予以實作 ::: ### Linux 核心風格的 List API 予以實作 教授有提供 Linux 核心風格 List API 的簡化標頭檔 list.h範例程式碼,但我決定用 lab0 專案中與使用的程式碼來操作,未來可方便直接用於 q_sort 函式之中,也可思考現階段的 mergesort 與此 quicksort 在各樣本數下速度的差異([Github連結](https://github.com/RealBigMickey/lab0-c/tree/master)) **[Mock-up 的完整程式碼](https://github.com/RealBigMickey/lab0-c/blob/master/quicksort_mock.c)** 邏輯與上面大同小異,只是細節上有些變化。首先會先斷掉 list 的尾巴,一律將所有 list 視為 singly linked list 來方便處理。 ![graph (4)](https://hackmd.io/_uploads/HkLfHXN2ke.png) 一切分割跟排序都完成後,再將走過一遍連結串列,將 singly linked list 的 prev 指標/修正,變回完整的 doubly linked list。 寫的過程中也是遇到非常多問題,一次又一次的 segmentation fault ,debug 也是花了不少的時間。 **示意圖[(來源)](https://hackmd.io/@sysprog/linux2025-quiz1#%E6%B8%AC%E9%A9%97-2):** ![image](https://hackmd.io/_uploads/Hy6Jk4VhJg.png) ![image](https://hackmd.io/_uploads/HJnlk4Enke.png) ![image](https://hackmd.io/_uploads/SkFZk4Nnke.png) ![image](https://hackmd.io/_uploads/ByEM1VEnkl.png) ![image](https://hackmd.io/_uploads/Sk6fJV431g.png) ![image](https://hackmd.io/_uploads/HkEmJE431e.png) 坦承 - 這邊非常誠實的澄清,我發現我最近幾個月開始有些過度依賴chatgpt,以至於自己寫的程式在執行前都要給它驗證一遍才敢執行。在寫這個 quicksort 寫到一半時碰壁,好幾小時在與 gpt 反覆辯論,但 gpt 就是抓不到錯誤,而我也沒有非常花心思去看自己的程式碼。我知道身邊許多資工人在不知不覺中也養成了這種壞習慣,過度依賴個很有瑕疵的工具。 掙扎許久後我把 gpt 關掉,打開 yt music 決定自己慢慢 debug,中間不斷插入 printf 看輸出,用小畫家來畫圖思考,幾小時候完成了程式。有時在想,如果從一開始就不這麼仰賴 gpt 來解題,如果不要那麼懶、更相信自己,是不是能夠更快寫出來? 本該是理所當然的事,現在才領悟。從今之後會警惕自己不要落入 gpt 的陷阱,多閱讀文獻、使用google,盡可能靠自己的力量去完成,成為一位有價值的資訊人。 ![image](https://hackmd.io/_uploads/SJU36mEnJe.png) # quiz2 ## 測驗一 ::: success 要求: - 解釋上方程式碼運作原理,改進 random_shuffle_array 也探討 list_for_each_entry_safe 建構的迴圈中,若將 list_move_tail 換為 list_move,為何會無法滿足 stable sorting - 將程式碼整合進 lab0 提及的 qtest,並探討環狀雙向鏈結串列的排序效能,並善用 [perf](https://hackmd.io/@sysprog/linux-perf) 一類的效能分析工具,探討包含 Linux 核心 [list_sort](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 在內排序實作的效能表現並充分解釋 ::: 一開始將 AAAA, BBBB, CCCC, DDDD, EEEE, FFFF 填入後將完整程式碼建成一 .c 檔,發現程式會卡在一迴圈中出不去。 Output: ![image](https://hackmd.io/_uploads/r1degSV2ye.png) *qsort 後執行 linkedlist quicksort,但遲遲無法完成* 問題出在 list_for_each_entry_safe 迴圈中,AAAA格的答案猜錯,應為 list_first_entry 才對: ```c // list_quicksort 函式中 pivot = container_of(head, struct listitem, list); list_del(&pivot->list); list_for_each_entry_safe (item, is, head, list) { //printf("going"); if (cmpint(&item->i, &pivot->i) < 0) list_move_tail(&item->list, &list_less); else list_move_tail(&item->list, &list_greater); } printf("checkpoint\n"); ``` ### 改善 random_shuffle_array 回想起 lab0 的其中一個要求 - 實做亂數產生器,其中就有提到 [Fisher–Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle)演算法,這邊也將使用此演算法 演算法邏輯: - 從最後一個元素 n 開始(標為i),此為元素一 - 利用 rand() % i + 1 取餘數找出元素二的位置,簡稱 j(rand 使用系統時間生成亂數) - 將 i 與 j 兩元素進行交換 - i - 1 ,以此類推直到 i 走到第一格 ![graph (5)](https://hackmd.io/_uploads/HJ5F7IE31g.png) ![graph (6)](https://hackmd.io/_uploads/rJdhXIE2Je.png) ![graph (7)](https://hackmd.io/_uploads/S1maXIE3Jg.png) ![graph (8)](https://hackmd.io/_uploads/Skx0Q8V3yg.png) ```c static inline void random_shuffle_array(uint16_t *operations, uint16_t len) { // Set every num to their respective number first for (uint16_t i = 0; i < len; i++) operations[i] = i + 1; // Fisher–Yates shuffle for (uint16_t i = len-1; i > 0; i--) { uint16_t j = rand() % (i + 1); uint16_t temp = operations[i]; operations[i] = operations[j]; operations[j] = temp; } } ``` ### 為何將 list_move_tail 換為 list_move 時,會無法滿足 stable sorting **stable sorting -> 當兩元素數值相同時,排序前後的順序兩元素的順序會維持不變** list_move_tail -> 將節點丟到 list 尾端 list_move -> 將節點丟到 list 頭端(dummy head 後) 假設一 list = [3, 1, <span style="color:blue">2</span>, <span style="color:red">2</span>, 4],2用不同顏色來方便區分 pivot 為 3,接下來得將元素們分為兩 list,Less 與 Greater ### **<span style="color:red">list_move_tail:</span>** Less: [1, <span style="color:blue">2</span>, <span style="color:red">2</span>] Great: [4] ### **<span style="color:red">list_move:</span>** Less: [<span style="color:red">2</span>, <span style="color:blue">2</span>, 1] Great: [4] 因為元素是依序丟入,所以藍色的2會先被處理而先加入清單,紅色的2則會比較晚加入,因此最後在藍色2的前方。 > *兩相同元素在排序前後改變了。* ### quicksort 融入 lab0 與新增 qtest 指令 將 listitem 改為 element_t,cmpint 改用 strcmp 基本上就能放入 lab0 的 queue.c 之中了。 在 queue.h 之中也得定義一行 ```c void list_quicksort(struct list_head *head); ``` 接下來進入 qtest.c ,新增一函式 do_quicksort ,程式碼這邊完全取自 do_sort 。來到 console_init 函式,新增一行: ```c ADD_COMMAND(quicksort, "**Assignment from N02** Sort queue in ascending order", ""); ``` 完成後重新執行 make 指令,測試 qtest : >![image](https://hackmd.io/_uploads/SyijQUH3kx.png) *help 畫面有 quicksort 指令* ![image](https://hackmd.io/_uploads/rydwNLB2Jl.png) *沒問題!* ![image](https://hackmd.io/_uploads/ByUYaLrnkx.png) 問題:這些改動 commit 到 lab0 中時會被擋下, 解決辦法:改把函式直接丟到 qtests 之中,並刪掉在 queue.h 裡的定義 ### 利用 perf 分析 ![image](https://hackmd.io/_uploads/r1GK0nH21l.png) ![image](https://hackmd.io/_uploads/HyXy16S3kx.png) ![image](https://hackmd.io/_uploads/ByS-Z6r2yl.png) 可看見這個 mov 指令旁邊寫了 100% ,但這不代表這個指令花費了這部份全部的處理器時間,而是這邊是熱點,每次檢查時都在這邊。 這意味著時間都花在這個迴圈之中,也就是走過連結串列的迴圈中,算是在預料之內。 **如何簡短時間?** 既然時間都花在走過各連結,那麼解決改善的方向也很簡單,減少需要在串列上走的時間。 :::info **我的想法:** 每次呼叫 list_move_tail 都得走遍整個連結串列找到尾巴,浪費時間。 - 使用 lise_move: - 優:省時 - 缺:破壞 stable sort - 改用雙向連結串列: - 優:省時,尾巴為 head->prev - 缺:節點結構得花更多記憶體 ::: ### Linux 核心內的 list_sort 為一種改良過的 merge sort ,與自己在 lab0 實做的合併排序最大的不同是 bits 這邊: ```c do { size_t bits; struct list_head **tail = &pending; /* Find the least-significant clear bit in count */ for (bits = count; bits & 1; bits >>= 1) tail = &(*tail)->prev; /* Do the indicated merge */ if (likely(bits)) { struct list_head *a = *tail, *b = a->prev; a = merge(priv, cmp, b, a); /* Install the merged result in place of the inputs */ a->prev = b->prev; *tail = a; } /* Move one element from input list to pending */ list->prev = pending; pending = list; list = list->next; pending->next = NULL; count++; } while (list); ``` 清單從後往前走,每次往 prev 指標前進,接下來將一部份一部份慢慢分析 ```c for (bits = count; bits & 1; bits >>= 1) ``` count 的二進位表示紀錄著當前大小為 2^k 的 list 在等待合併,k 為第 K 位元 bits & 1 檢查要不要繼續前進 ```c #define likely(x) __builtin_expect(!!(x), 1) ``` if(likely(bits)) 與 if(bits) 邏輯上是一樣的,但是使用巨集可告知編譯器預期結果為 true ,從而進一步去優化此程式 接下來把下一個該處理的節點存入 pending,並斷掉它的 next 指標,最後 count + 1 簡單卻很有效的用 count 跟 bit minipulation 去紀錄是否有待合併清單,在有相同大小清單時迅速做出合併,減少整體比較次數、也降低 cpu & 記憶體的開銷。 ## 測驗二 :::success 要求: 1. 解釋上述程式碼,並探討擴充為 $\lceil \sqrt{x} \rceil$(向上取整數) 實作,該如何調整並保持只用整數加減及位元操作 2. 參照[計算機結構測驗題 C](https://hackmd.io/@sysprog/arch2024-quiz3-sol#Problem-C) 及其[注釋](https://hackmd.io/@sysprog/HJKRbSAVJl#Problem-C30),以 C 語言 (搭配 GNU extension) 撰寫不依賴除法指令的 `sqrtf`,確保符合 IEEE 754 規格。對照 [sqrtf, rsqrt, reciprocal implementations in Arm assembly](https://github.com/picolibc/picolibc/issues/956) 3. 參照[從 √2 的存在談開平方根的快速運算](https://hackmd.io/@sysprog/sqrt)提到的「藉由查表的平方根運算」,以 C 語言實作,並比較不同手法的整數開平方根效能 - 一旦發現效能改進的機會,可準備提交改進 `int_sqrt` 程式碼到 Linux 核心 ::: ### clz(), clz32(), clz64() ![image](https://hackmd.io/_uploads/rkChy6I6kl.png) ```c unsigned clz2(uint32_t x, int c) { if (!x && !c) return 32; uint32_t upper = (x >> (16 >> c)); uint32_t lower = (x & (0xFFFF >> mask[c])); if (c == 3) return upper ? magic[upper] : 2 + magic[lower]; return upper ? clz2(upper, c + 1) : (16 >> (c)) + clz2(lower, c + 1); } ``` 函式 clz2 不外乎目的就是 count leading zeros ,利用遞迴不斷的分割成上下部份,直到上部份與下部份剩 2 位元後再利用 magic[] 查表計算出結果 呼叫前都得傳入個 0 初始話,利用巨集省麻煩 -> clz32 x >> 16 分離出上半部份,利用 AND 跟查表分離出下半部份。在 c == 3 之前不斷分割。若要計算 64 位元的整數,則手動分割成上下部份再傳入 clz32 兩次 ![graph (9)](https://hackmd.io/_uploads/HyeJwu8nJe.png) ### 整數平方根 sqrti() ```c uint64_t sqrti(uint64_t x) { uint64_t m, y = 0; if (x <= 1) return x; int total_bits = 64; /* clz64(x) returns the count of leading zeros in x. * (total_bits - 1 - clz64(x)) gives the index of the highest set bit. * Rounding that index down to an even number ensures our starting m is a * power of 4. */ int shift = (total_bits - 1 - clz64(x)) & ~1; m = 1ULL << shift; while (m) { uint64_t b = y + m; y >>= 1; if (x >= b) { x -= b; y += m; } m >>= 2; } return y; } ``` ~1 = 0xFFFFFFFFFFFFFFFE,除了 LSB 其餘位元均為1 ,可確保 shift 為4的倍數,m 為最接近但小於 x 的4的倍數 從最大的候選位元開始,若加入後仍小於 x , ,則存入 y 之中,反之不存,m 再/4。每一次迴圈中y會往右一格,因而依序存下每個位元 ### $\lceil \sqrt{x} \rceil$ 向上取整數 在return y 前加這一小段就可以實現: ```c if(x) return y+1; ``` 看起來簡單但也是想了一陣子,我是假設兩數字 144 與 143 並紀錄 sqrti 過程中的每一步驟中的x, y 與 m 數值並去尋找規律,而發現當剛好為平方數時最後會 x = 0 ![image](https://hackmd.io/_uploads/rJUPCFUnJe.png) **e.g. 144**: ::: spoiler **iteration 1: `x = 144, y = 0, m = 64`** - b = 0 + 64 = 64 - y >>= 1 = 0 - x >= b is true: - x = 144 - 64 = 80 - y = 0 + 64 = 64 - m >>= 2 = 16 **iteration 2: `x = 80, y = 64, m = 16`** - b = 64 + 16 = 80 - y >>= 1 = 32 - x >= b is true: - x = 80 - 80 = 0 - y = 32 + 16 = 48 m >>= 2 = 4 **iteration 3: `x = 0, y = 48, m = 4`** - b = 48 + 4 = 52 - y >>= 1 = 24 - x >= b is **false** - m >>= 2 = 16 **iteration 4: `x = 0, y = 24, m = 1`** - b = y + m = 24 + 1 - y >>= 1 = 12 - x >= b is **false** - m >>= 2 = 0 > x is 0 when the loop ends! ::: **e.g. 143**: ::: spoiler **iteration 1: `x = 143, y = 0, m = 64`** - b = 0 + 64 = 64 - y >>= 1 = 0 - x >= b is true: - x = 143 - 64 = 79 - y = 0 + 64 = 64 - m >>= 2 = 16 **iteration 2: `x = 79, y = 64, m = 16`** - b = 64 + 16 = 80 - y >>= 1 = 32 - x >= b is false - m >>= 2 = 4 **iteration 3: `x = 79, y = 32, m = 4`** - b = 32 + 4 = 36 - y >>= 1 = 16 - x >= b is true: - x = 79 - 36 = 43 - y = 16 + 4 = 20 - m >>= 2 = 1 **iteration 4: `x = 43, y = 20, m = 1`** - b = 20 + 1 = 21 - y >>= 1 = 10 - x >= b is true: - x = 43 - 21 = 22 - y = 10 + 1 = 11 - m >>= 2 = 0 > x is 22 when the loop ends! ::: ### 撰寫不依賴除法指令的 sqrtf **這邊提供 sqrtf1, sqrtf2, sqrtf3 三個函式** *- 大同小異,只有微小的差異* #### sqrtf1: ```c float sqrtf1(float number) { float y = number; uint32_t i = *(uint32_t*)&y; i = (i>>1) + 0x1fc00000; y = *(float*)&i; return y; } ``` 參照 quake3 中的 [fast_inverse_sqrt](https://en.wikipedia.org/wiki/Fast_inverse_square_root),我想用差不多的作法來尋找平方根! 首先,原想起浮點數的定義,1 位元為sign、 8位元為 Exponent、23位元為Mantissa: 平方根恆正,所以 sign bit 為0 因此 bit representaion 為 E & M或是 2^23*E + M ,而浮點數的實際數值為(1+M/2^23 )*2^(E-127) ![image](https://hackmd.io/_uploads/SkKCMkuhJe.png) 接下來,我們它取log2: ![image](https://hackmd.io/_uploads/H1iyXkd21g.png) 變成 log2(1+M/2^23) + E - 127後,數字乍看之下變得比較難處理,但回想起大一微積分: 當 x 趨近 0,log(1+x) ~= x 話一張圖表示曲線與直線,若平移直線一咪咪可以讓0到1之間的預測值誤差控制一些,使0到1之間的預測值平均誤差控制到最小,μ = 0.043 ![image](https://hackmd.io/_uploads/rJt7w1u21e.png) 帶進去後移項我們可以發現 M+2^23*E又出現了,沒錯這就是前面寫道的bit representaion,得到結論: **某種程度上,浮點數的log2 約略為它的 bit representaion 移位後的數值!** ![image](https://hackmd.io/_uploads/Hykcukdhkg.png) 前置作業完成,可以開始計算了: ![image](https://hackmd.io/_uploads/rJY1ty_hyl.png) 這邊的目標是尋找 magic number,其實就只是計算式子+移項後的結果。 **測試用函式: ** ```c #define N_TESTS 100000 float testValues[N_TESTS]; for (int i = 0; i < N_TESTS; i++) { testValues[i] = (float)(i + 1); } double error_tot = 0.0; float max_error = 0.0; //printf("Value\tFastSqrt\tSqrt\t\tError (%%)\n"); //printf("-----------------------------------------------------\n"); for (int i = 0; i < N_TESTS; i++) { float value = testValues[i]; float approx = sqrtf1(value); float correct = sqrt(value); float errorPercent = fabs(approx - correct) / correct * 100.0f; //printf("%-6.2f\t%-9.4f\t%-9.4f\t%-8.2f\n", value, approx, correct, errorPercent); error_tot += errorPercent; if(errorPercent > max_error) max_error = errorPercent; } printf("Error avg: %.6f%%\n", error_tot/N_TESTS); printf("Error max: %.6f%%\n", max_error); ``` ![image](https://hackmd.io/_uploads/HyvMAyuhyl.png) 誤差不能說小,但在一些不要求精準度的情況下這樣或許就夠了,但若能再準一點就好了。 sqrtf1 耗時: 0.306210 seconds #### sqrtf2: 引入牛頓法,設 f(y) = 0 = y^2 - x ,則新 y = y - f(y)/f'(y),推導後得出 y/2 + x/2y ![image](https://hackmd.io/_uploads/HyGgkgOn1l.png) 加上: ```c // Newton's method to get better approx y = (y + number/y)*0.5; ``` ![image](https://hackmd.io/_uploads/SkSAQlu3Jg.png) 誤差減少了許多,但因為使用了除法,耗時也增加了不少。 sqrtf2 耗時: 0.414774 seconds #### sqrtf3: 回想起 quake3 中的 fast_inverse_square,其解 y = x^-1/2 乘上x不就是x^1/2了嗎? ```c float sqrtf3( float number ) { long i; float x2, y; const float threehalfs = 1.5F; x2 = number * 0.5F; y = number; i = * ( long * ) &y; i = 0x5f3759df - ( i >> 1 ); y = * ( float * ) &i; y = y * ( threehalfs - ( x2 * y * y ) ); return y*number; } ``` 中間邏輯與1 2 一樣,還沒有使用到除法,應為最快的吧? 但實際上 sqrt3 卻是三者之間最慢的函式,而且還慢不少。 sqrtf3 耗時: 1.453131 seconds :::info **我的猜測:** ```c y = y * ( threehalfs - ( x2 * y * y ) ); ``` 與 ```c return y*number; ``` 須花太多時間運算,多次浮點數乘法運算會比一次的除法還要花上更多時間 ::: **測時間函式:** ```c #define NUM_TESTS 100000000 double get_time() { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); return t.tv_sec + t.tv_nsec * 1e-9; } // time = start - end times ``` **結論:** sqrt2 準確且快速,但還是會輸 math.h 的 sqrt函式😢。 具體原因不確定,猜測是因為 math.h 的 sqrt 會動用處理器專門運算平方根的電路,程式不可能比單一功能的邏輯閘矩陣還快... ## 測驗三 :::success 要求: - 解釋上述程式碼運作原理,應提供測試程式碼 >針對 [Linux 核心的 hash table](https://hackmd.io/@sysprog/linux-hashtable) 實作,提供更多圖例並揣摩 Linux 核心開發者 - 進行《[The Art of Computer Programming](https://www-cs-faculty.stanford.edu/~knuth/taocp.html)》(vol 3) 一書section 6.4 的 exercise 8,也就是證明 Theorem S >注意: Linux 核心內部大量使用 hash table,但並非每處的雜湊函數都滿足減少碰撞及避免 clustering 現象的原則,也就是存在若干改進空間 - 探討 Linux 核心中使用 hash table 的應用案例,至少二項,應當列出原始程式碼並分析,斟酌搭配 git log 以得知 Linux 核心開發者變更程式碼的考量因素 ::: 這邊的 hashtable 實作沒有 bucket 大小上的限制,因此也沒有 collision (碰撞)的問題。每個bucket 會有個 hlist_head ,而 hlist_head 再指向第一個鍊結串列節點 **Hash function** 利用黃金比例生出 hash: ```c #define GOLDEN_RATIO_32 0x61C88647 static inline unsigned int hash(unsigned int val, unsigned int bits) { /* High bits are more random, so use them. */ return (val * GOLDEN_RATIO_32) >> (32 - bits); } ``` 黃金比例中的數字(尤其是高位元的)分佈還算隨機,簡單的乘上val再平移可以還算均勻的分佈每個元素,是個常見的 hash function ### pprev 剛開始腦袋有點轉不過來,不理解 pprev 是拿來做什麼的,就好比 prev 是上一個節點的指標,我以為 pprev 上一個會儲存上一個節點指標的地址 實際上 pprev 要儲存著上一個指標的 next 指標的地址,為的是在刪除節點時能夠更方便且迅速的操作 ```c // 若要刪除一節點 node ,兩行就能解決 *node->pprev = node->next; free(node); ``` ![image](https://hackmd.io/_uploads/Hk2Er4t2yl.png) ![image](https://hackmd.io/_uploads/HJpmSNY31g.png) ### 什麼是 map_t 中的 bits? 結構 map_t 中有個叫 bits 的整數: ```c typedef struct { int bits; struct hlist_head *ht; } map_t; ``` hashtable 中的 bucket count 就是由 bits 推出來,bucket_count = 1 << bits 。這麼做在操作時可依賴 bit operation 從而加速 hash 等函式的執行速度,也因為為2的倍數而在記憶體配置上有一咪咪的優勢 map_init配置記憶體並回傳一初始化的 map_t ![image](https://hackmd.io/_uploads/SkNOYOthJx.png) hash 尋輸入值對應的 hash 值 find_key 到 hash table 中對應的 bucket,走遍連結串列尋找對應 key 的 hash_key ![image](https://hackmd.io/_uploads/SyccF_K2ye.png) map_add 先檢查此 key 是否已存在,若無則配置記憶體並將它插入 bucket 中 first 的位置,若已被佔據則將舊節點往後推 ![image](https://hackmd.io/_uploads/ryo2K_thye.png) 最後 map_deinit 負責刪除整個結構並釋放所有被配置的記憶體 ![image](https://hackmd.io/_uploads/ryNvcdt2yx.png) ## Linux 核心中使用 hash table 的應用案例 ### Page Tables: Paging 或是分頁是記憶體管理的一部分,將空間分科成一塊一塊固定大小,再做記憶體與虛擬記憶體的管理與交換。需要時將資源帶到記憶體中,待機時資源送到虛擬記憶/硬碟中。 Linux 核心中共有4種等級的 Page Tables 1. Page Global Directory (PGD) - pgd_t/pgdval_t. 2. Page Upper Directory (PUD) - pud_t/pudval_t. 3. Page Middle Directory (PMD) - pmd_t/pmdval_t. 4. Page Table Entry directory (PTE) - pte_t/pteval_t. ```c // 定義 typedef unsigned long pgdval_t; typedef unsigned long pudval_t; typedef unsigned long pmdval_t; typedef unsigned long pteval_t; typedef struct { pgdval_t pgd; } pgd_t; typedef struct { pudval_t pud; } pud_t; typedef struct { pmdval_t pmd; } pmd_t; typedef struct { pteval_t pte; } pte_t; ``` pgdval_t 為32位元,因此使用 unsigned long,再用 pgd_t 將其包住,這麼做雖然看起來沒意義,但卻是有些好處。 > 1. 型別安全:一個期待傳入 pgd_t 的函式無法不小心被傳入 pmd_t > 2. 抽離:若哪天得改變核心中的 page global directory,則改變定義就行,不必大幅更動程式碼 > 3. 直觀:一眼就能知道程式在操作哪個部份的 paging ,而不會將四個稿混 注意到了嗎?分頁就是很多個 hash table 綁在一起,一層一層的排列,好比千層麵 #### 多層 Hash Table 結構的好處 **節省空間:** 如果直接用一個巨大的單級頁表,則會產生大量的空洞(sparse mapping)、浪費記憶體。分層的結構使在虛擬地址空間的某個區段被實際使用時,才會被分配相應的下層頁表,從而更有效地利用內存 **搜尋速度:** CPU 將虛擬地址轉為實際地址時,會依次通過各層,從 PGD 層開始,根據虛擬地址的位元找到對應的 entry。再利用這個 entry 指向下一層(PUD)的頁表,重複同樣的查找過程。一直到 PTE 層,最終獲得具體的實際頁面地址。 因為只是在多個小型 hash table 中尋找,因此理論上每步驟都是O(1)常數時間,所以整體搜尋效率還是很高的。 ![image](https://hackmd.io/_uploads/HkQ6lnK21x.png) [Linux 核心 Github](https://github.com/torvalds/linux/blob/v4.6/arch/x86/include/asm/pgtable_types.h) ### Kobject [Linux 核心 Github](https://github.com/torvalds/linux/blob/master/lib/kobject.c) kobject 為核心中用來表示在 user space 的核心物件的一種概念(?): - 每個 kobject 的週期都有個管理者,當操作完畢或是 scope 改變就會釋放記憶體 - kobject 像樹狀結構那樣整理,且每個物件都有自己類似ID 的獨特辨認串與指向 parent kobject 的指標 切入主題,在 lib/kobject.c 中,kobject 主要是以鏈結串列加入到對應的 kset 中,但搜尋時時,為了加速搜尋,sysfs 的基層 kernfs 會用 hash table 來尋找這些物件 e.g. ```c struct kobject *kset_find_obj(struct kset *kset, const char *name) { struct kobject *k; struct kobject *ret = NULL; spin_lock(&kset->list_lock); list_for_each_entry(k, &kset->list, entry) { if (kobject_name(k) && !strcmp(kobject_name(k), name)) { ret = kobject_get_unless_zero(k); break; } } spin_unlock(&kset->list_lock); return ret; } EXPORT_SYMBOL_GPL(kset_find_obj); ``` 為的就是 hash table 那快速 O(1) 的查搜尋速度 kobject namespace 操作與固定陣列 另一個和 hash table 概念相關的例子在於 kobject 的命名空間管理。內核中有一個固定大小的陣列,用來存放不同命名空間類型的操作函式指標: ```c static DEFINE_SPINLOCK(kobj_ns_type_lock); static const struct kobj_ns_type_operations *kobj_ns_ops_tbl[KOBJ_NS_TYPES]; ``` 開發者透過陣列,可以根據 enum kobj_ns_type 操作在O(1)內查詢到對應的命名空間。這就類似於一個簡化的 hash table,其鍵值也是固定,提供了非常快的查找速度。因 kobj_ns_type 的大小有限,所以可直接使用陣列省麻煩。