# 2025q1 Homework2 (quiz1+2) contributed by < [`charliechiou`](https://github.com/charliechiou?tab=repositories) > {%hackmd NrmQUGbRQWemgwPfhzXj6g %} ## [第一週測驗題](https://hackmd.io/@sysprog/linux2025-quiz1) ### 測驗 `1` #### 延伸問題 1 >解釋上方程式碼運作原理 >"a pointer to a pointer" 使用指標指向想要操作的指標,避免了類似**一次一次把 current point 指向下一個節點**的這種操作。 先定義所用到的結構 ```c #include <stddef.h> typedef struct list_item { int value; struct list_item *next; } list_item_t; typedef struct { struct list_item *head; } list_t; ``` 其中 list_t 為整個鍊結串列結構,而 list_item 則為鍊結串列中的每個節點。示意圖如下 ```graphviz digraph LinkedList { node [shape=record]; rankdir=LR; list [label="list_t | <head>head"]; node1 [label="list_item |value:10| <next> next"]; node2 [label="list_item |value:10| <next> next"]; list:head -> node1; node1:next -> node2; node2:next -> NULL [style=dashed]; } ``` 測驗題實作 `list_insert_before` 的函式,目標為將 `*item` 插入 `*before` 之前。 ```c== void list_insert_before(list_t *l, list_item_t *before, list_item_t *item) { list_item_t **p; for (p = &l->head; *p != before; p = &(*p)->next); *p = item; item->next = before; } ``` 程式碼第 5 行定義 pointer to pointer 變數並於第 6 行使用迴圈將其指向目標位置前一個節點的 next (即 before 前一個節點的 next)。 ```graphviz digraph LinkedList { node [shape=record]; rankdir=LR; p [label="**p",shape=plaintext] list [label="<head>head"]; node1 [label="value:10| <next> next"]; node2 [label="before | value:10| <next> next"]; item [label="item|value:30|<next>next"]; list:head -> node1; node1:next -> node2; node2:next -> NULL [style=dashed]; p->node1:next; } ``` 接著第 7 行便將 p 所指向位置的指標設為指向欲插入的節點 item,即把 next 指標所指向的節點更換成新的節點 item。 ```graphviz digraph LinkedList { node [shape=record]; rankdir=LR; p [label="**p",shape=plaintext] list [label="<head>head"]; node1 [label="value:10| <next> next"]; node2 [label="before | value:10| <next> next"]; item [label="item|value:30|<next>next"]; list:head -> node1; node1:next -> item; node2:next -> NULL [style=dashed]; p->node1:next; } ``` 最後第 8 行再將 item 的 next 指向 before,並完成插入節點的目的。 ```graphviz digraph LinkedList { node [shape=record]; rankdir=LR; p [label="**p",shape=plaintext] list [label="<head>head"]; node1 [label="value:10| <next> next"]; node2 [label="before | value:10| <next> next"]; item [label="item|value:30|<next>next"]; list:head -> node1; node1:next -> item; item:next -> node2; node2:next -> NULL [style=dashed]; p->node1:next; } ``` 接著使用測試程式來測試結果,首先先建立 N 個測試用的節點,並使用 `list_reset` 初始化 ```c static list_t *list_reset(void) { for (size_t i = 0; i < N; i++) { items[i].value = i; items[i].next = NULL; } l.head = NULL; return &l; } ``` 根據規格書中 7.17 >size_t which is the unsigned integer type of the result of the sizeof operator size_t 為無符號整數型別,因此可用於 i 避免出現負數。接著便進行測試,分別測試將 item 插入在鍊結串列的頭,尾,並測試是否如預期的將其插入在對應的位置。 分別測試插入在 head 節點及尾端。 ```c /* Test inserting at the end */ list_reset(); my_assert(list_size(&l) == 0, "Initial list size is expected to be zero."); for (size_t i = 0; i < N; i++) list_insert_before(&l, NULL, &items[i]); my_assert(list_size(&l) == N, "Final list size should be N"); k = 0; cur = l.head; while (cur) { my_assert(cur->value == k, "Unexpected list item value"); k++; cur = cur->next; } ``` 了解完測試的方式接著查看如何使用 Marco 來將整個測試包裝,首先定義 `my_run_test` 作為測試的入口,接著在 `test_suite` 中傳入要進行的測試。 ```c #define my_run_test(test) \ do { \ char *message = test(); \ tests_run++; \ if (message) \ return message; \ } while (0) static char *test_suite(void) { my_run_test(test_list); return NULL; } ``` 以上述為例,test_list 傳入預先定義好的 my_run_test 後會被展開如下,透過這樣的方式便可以自由的更換我們所要測試的內容,而測試過程中若有錯誤則會回傳 message 並由 result 儲存錯誤訊息於最後印出。 ```c do { \ char *message = test_list(); \ tests_run++; \ if (message) \ return message; \ } while (0) ``` #### 延伸問題 2 >在現有的程式碼基礎上,撰寫合併排序操作 ### 測驗 `2` #### 延伸問題 1 >解釋上方程式碼運作的原理,並嘗試補完程式碼,使其可獨立執行,應提供相關的測試程式碼 首先先定義每個節點所儲存的資料,分別表示該節點所記憶的空閒大小、樹的左右兩個節點。 ```c typedef struct block { size_t size; struct block *l, *r; } block_t; ``` ```graphviz digraph BlockTree { node [shape=record]; block1 [label="<block_t>block_t | size: 32 | <l> l | <r> r"]; block2 [label="<block_t>block_t | size: 16 | <l> l | <r> r"]; block3 [label="<block_t>block_t | size: 48 | <l> l | <r> r"]; block1:l:s -> block2:block_t:n; block1:r:s -> block3:block_t:n; } ``` 接著使用二元樹的方式做搜尋,使用到的函式分別為 `find_free_tree`, `remove_free_tree`, `insert_free_tree` 。 `find_free_tree` 用於尋找 free tree 中可以使用的空間,當需要分配記憶體時便會用到該函式來確認是否有足夠的空間可以分配記憶體。 假設目前 free tree 結構如下,其中 size 表示剩餘的空間大小。 ```graphviz digraph BlockTree { node [shape=record]; block1 [label="size: 500 | <l> l | <r> r"]; block2 [label="size: 200 | <l> l | <r> r"]; block3 [label="size: 600 | <l> l | <r> r"]; block4 [label="size: 100 | NULL"]; block5 [label="size: 300 | NULL"]; block6 [label="size: 550 | NULL"]; block1:l -> block2:n; block1:r -> block3:n; block2:l -> block4:n; block2:r -> block5:n; block3:l -> block6:n; block3:r -> NULL:n; } ``` 先將 `**curr = root` 指向根節點及 `**best_fit` 指向 NULL,並開始搜尋。 ```graphviz digraph BlockTree { node [shape=record]; curr[label="**curr",shape=plaintext] best_fit[label="**best_fit",shape=plaintext] null[label="NULL",shape=plaintext] block1 [label="size: 500 | <l> l | <r> r"]; block2 [label="size: 200 | <l> l | <r> r"]; block3 [label="size: 600 | <l> l | <r> r"]; block4 [label="size: 100 | NULL"]; block5 [label="size: 300 | NULL"]; block6 [label="size: 550 | NULL"]; curr -> block1:w; best_fit -> null block1:l -> block2:n; block1:r -> block3:n; block2:l -> block4:n; block2:r -> block5:n; block3:l -> block6:n; block3:r -> NULL:n; } ``` 以分配 target = 350 為例,當 curr 所指向的大小大於 target 時,由於 curr 已經足夠分配大小了,因此更新 best_fit 指向 curr 的位置,並持續朝節點的左邊更新 curr 試圖尋找更小且足夠分配的空間。 ```c best_fit = curr; curr = &((*curr)->l); ``` ```graphviz digraph BlockTree { node [shape=record]; curr[label="**curr",shape=plaintext] best_fit[label="**best_fit",shape=plaintext] block1 [label="size: 500 | <l> l | <r> r"]; block2 [label="size: 200 | <l> l | <r> r"]; block3 [label="size: 600 | <l> l | <r> r"]; block4 [label="size: 100 | NULL"]; block5 [label="size: 300 | NULL"]; block6 [label="size: 550 | NULL"]; curr -> block2:w; best_fit -> block1:n; block1:l -> block2:n; block1:r -> block3:n; block2:l -> block4:n; block2:r -> block5:n; block3:l -> block6:n; block3:r -> NULL:n; } ``` 當目前節點小於 target 時,由於空間不夠分配給使用者因此不改動 best_fit 的位置,繼續更新 curr 至節點的右邊尋找。 ```graphviz digraph BlockTree { node [shape=record]; curr[label="**curr",shape=plaintext] best_fit[label="**best_fit",shape=plaintext] block1 [label="size: 500 | <l> l | <r> r"]; block2 [label="size: 200 | <l> l | <r> r"]; block3 [label="size: 600 | <l> l | <r> r"]; block4 [label="size: 100 | NULL"]; block5 [label="size: 300 | NULL"]; block6 [label="size: 550 | NULL"]; curr -> block5; best_fit -> block1; block1:l -> block2; block1:r -> block3; block2:l -> block4; block2:r -> block5; block3:l -> block6; block3:r -> NULL:n; } ``` ```graphviz digraph BlockTree { rankdir=TB ordering=out node [shape=record]; curr[label="**curr",shape=plaintext] best_fit[label="**best_fit",shape=plaintext] block1 [label="size: 500 | <l> l | <r> r"]; block2 [label="size: 200 | <l> l | <r> r"]; block3 [label="size: 600 | <l> l | <r> r"]; block4 [label="size: 100 | NULL"]; block5 [label="size: 300 | NULL"]; block6 [label="size: 550 | NULL"]; curr -> block5; best_fit -> block5; block1:l -> block2; block1:r -> block3; block2:l -> block4; block2:r -> block5; block3:l -> block6; block3:r -> NULL:n; } ``` 當 curr 已經指向 NULL 時候便回傳最後所紀錄到的 best_fit。 若過程中直接找到 target 則直接會傳 curr。 `remove_free_tree` 用於將特定節點從 free tree 中移除,並調整好樹的結構使其維持結構。 首先先使用前述提到過的函式來找到目標的位置 ```c block_t **node_ptr = find_free_tree(root, target); ``` 此時會有三種可能性 - 沒有子節點 - 存在一個子節點 - 存在兩個子節點 **沒有子節點**以刪除 size = 300 的節點為例, node_ptr 指向欲刪除的節點 ```graphviz digraph BlockTree { rankdir=TB ordering=out node [shape=record]; node_ptr[label="**node_ptr",shape=plaintext] block1 [label="size: 500 | <l> l | <r> r"]; block2 [label="size: 200 | <l> l | <r> r"]; block3 [label="size: 600 | <l> l | <r> r"]; block4 [label="size: 100 | NULL"]; block5 [label="size: 300 | NULL"]; block6 [label="size: 550 | NULL"]; node_ptr -> block5:n; block1:l -> block2:n; block1:r -> block3:n; block2:l -> block4:n; block2:r -> block5:n; block3:l -> block6:n; block3:r -> NULL:n; } ``` 由於沒有子節點,因此直接將 node_ptr 改為指向 NULL 以移除節點,且不需要做調整。 **存在一個子節點**以刪除 size=600 的節點為例, node_ptr 指向欲刪除的節點。 ```graphviz digraph BlockTree { rankdir=TB ordering=out node [shape=record]; node_ptr[label="**node_ptr",shape=plaintext] block1 [label="size: 500 | <l> l | <r> r"]; block2 [label="size: 200 | <l> l | <r> r"]; block3 [label="size: 600 | <l> l | <r> r"]; block4 [label="size: 100 | NULL"]; block5 [label="size: 300 | NULL"]; block6 [label="size: 550 | NULL"]; node_ptr -> block3:e; block1:l -> block2:n; block1:r -> block3:n; block2:l -> block4:n; block2:r -> block5:n; block3:l -> block6:n; block3:r -> NULL:n; } ``` 先判斷節點在 node_ptr 的左邊或右邊,再直接將 node_ptr 指向該節點以完成取代。 ```c block_t *child = ((*node_ptr)->l) ? (*node_ptr)->l : (*node_ptr)->r; *node_ptr = child; ``` **存在兩個子節點**以刪除 size=200 為例, node_ptr 指向欲刪除的節點。 ```graphviz digraph BlockTree { rankdir=TB ordering=out node [shape=record]; node_ptr[label="**node_ptr",shape=plaintext] block1 [label="size: 500 | <l> l | <r> r"]; block2 [label="size: 200 | <l> l | <r> r"]; block3 [label="size: 600 | <l> l | <r> r"]; block4 [label="size: 100 | NULL"]; block5 [label="size: 300 | NULL"]; block6 [label="size: 550 | NULL"]; node_ptr -> block2:w; block1:l -> block2:n; block1:r -> block3:n; block2:l -> block4:n; block2:r -> block5:n; block3:l -> block6:n; block3:r -> NULL:n; } ``` 使用左子樹的最大值 predecessor 來取代 node_ptr 位置,由於 predecessor 大於左子樹中所有值且小於右子樹所有值,因此取代後仍然滿足下列兩點二元樹的要求。 - 根節點大於左子樹所有節點 - 根節點小於右子樹 透過尋找左子樹最右邊的節點來確認 predecessor 的位置 ```c block_t **pred_ptr = &(*node_ptr)->l; while ((*pred_ptr)->r) pred_ptr = &((*pred_ptr)->r); ``` 若 predecessor 即為 node_ptr 左邊節點,直接替換 ```c block_t *old_right = (*node_ptr)->r; *node_ptr = *pred_ptr; /* Replace target with its left child. */ (*node_ptr)->r = old_right; /* Attach the original right subtree. */ ``` ```graphviz digraph BlockTree { rankdir=TB ordering=out node [shape=record]; block1 [label="size: 500 | <l> l | <r> r"]; block3 [label="size: 600 | <l> l | <r> r"]; block4 [label="size: 100 | NULL"]; block5 [label="size: 300 | NULL"]; block6 [label="size: 550 | NULL"]; block1:l -> block4:n; block4:r -> block5:n; block1:r -> block3:n; block3:l -> block6:n; block3:r -> NULL:n; } ``` 若 predecessor 在左子樹更深的位置,則先儲存 node_ptr 左右兩個節點位置 ```c block_t *old_left = (*node_ptr)->l; block_t *old_right = (*node_ptr)->r; block_t *pred_node = *pred_ptr; ``` 並迭代呼叫 `remove_free_tree` 再使用移除的 predecessor 來取代 node_ptr 的位置 ```c /* The predecessor is deeper in the left subtree. */ block_t *old_left = (*node_ptr)->l; block_t *old_right = (*node_ptr)->r; block_t *pred_node = *pred_ptr; /* Remove the predecessor from its original location. */ remove_free_tree(&old_left, *pred_ptr); /* Replace the target node with the predecessor. */ *node_ptr = pred_node; (*node_ptr)->l = old_left; (*node_ptr)->r = old_right; assert(*node_ptr != (*node_ptr)->l); assert(*node_ptr != (*node_ptr)->r); ``` `insert_free_tree` 用於在樹中插入新節點 以插入 size=250 為例,先將 curr 指向根節點 ```graphviz digraph BlockTree { node [shape=record]; curr[label="**curr",shape=plaintext] target [label="size: 250 | <l> l | <r> r"]; block1 [label="size: 500 | <l> l | <r> r"]; block2 [label="size: 200 | <l> l | <r> r"]; block3 [label="size: 600 | <l> l | <r> r"]; block4 [label="size: 100 | NULL"]; block5 [label="size: 300 | NULL"]; block6 [label="size: 550 | NULL"]; curr -> block1; block1:l -> block2:n; block1:r -> block3:n; block2:l -> block4:n; block2:r -> block5:n; block3:l -> block6:n; block3:r -> NULL:n; } ``` 若目標小於 curr 則向左尋找可插入的位置,反之則向右尋找。 ```graphviz digraph BlockTree { node [shape=record]; curr[label="**curr",shape=plaintext] target [label="size: 250 | <l> l | <r> r"]; block1 [label="size: 500 | <l> l | <r> r"]; block2 [label="size: 200 | <l> l | <r> r"]; block3 [label="size: 600 | <l> l | <r> r"]; block4 [label="size: 100 | NULL"]; block5 [label="size: 300 | <l> l | <r> r"]; block6 [label="size: 550 | NULL"]; curr -> block5:r; block1:l -> block2:n; block1:r -> block3:n; block2:l -> block4:n; block2:r -> block5:n; block3:l -> block6:n; block3:r -> NULL:n; } ``` 尋找到可插入的位置後便把該位置替換為 node,並將左右節點初始化為 0 。 ```c *curr = node; node->l = node->r = NULL; ``` ```graphviz digraph BlockTree { node [shape=record]; target [label="size: 250 | <l> l | <r> r"]; block1 [label="size: 500 | <l> l | <r> r"]; block2 [label="size: 200 | <l> l | <r> r"]; block3 [label="size: 600 | <l> l | <r> r"]; block4 [label="size: 100 | NULL"]; block5 [label="size: 300 | <l> l | <r> r"]; block6 [label="size: 550 | NULL"]; block7 [label="size: 350 | NULL"]; NULL1 [label="NULL"]; NULL2 [label="NULL"]; block1:l -> block2:n; block1:r -> block3:n; block2:l -> block4:n; block2:r -> block5:n; block3:l -> block6:n; block3:r -> NULL1:n block5:r -> block7:n; block5:l -> NULL2:n; } ``` 接著便進行 allocator 的實作 首先先使用 `init_allocator` 初始化記憶體空間,使用 malloc 來分配所需要的大小並將空間存入 free tree。 由於會需要使用 block_t 的記憶體來紀錄 free tree 相關資訊,因此存入的空間大小需要預先扣除 `sizeof(block_t)` 才會是使用者真正可以分配的空間。 ```c free_tree_root->size = total_size - sizeof(block_t); ``` 接著使用 allocate 及 deallocate 兩個函式來分配及釋放記憶體空間。 `allocate` 首先先使用 `find_free_tree` 來找到可以用來分配的 block_t ,接著計算分配後的 block_t 記憶體位置並使用 insert_free_tree 增加至 free tree 中。 ```c block_t *new_block = (block_t *)((char *)found + sizeof(block_t) + size); ``` 新的記憶體位置為原始找到的地址加上 block_t 結構大小及分配的空間大小。 ```graphviz digraph MemoryAllocation { node [shape=record]; FoundBlock [label="<block_t>block_t (metadata) | 分配的記憶體空間 (size bytes) | <free_space>剩餘的空閒空間", shape=record]; FoundPointer [label="found", shape=plaintext]; new_block [label="*new_block", shape=plaintext]; FoundPointer -> FoundBlock:block_t[headport=w]; new_block -> FoundBlock:free_space } ``` 最後再將原先 tree 上的 node 移除 ```c remove_free_tree(&free_tree_root, found); ``` `deallocate` 則直接先找回需要釋放的記憶體位置 ```c block_t *block = (block_t *)ptr - 1; ``` 接著把多出來的空間再重新紀錄於 free_tree 上。 ```c insert_free_tree(&free_tree_root, block); ``` 最後使用測試程式來測試分配結果 ```c int main() { printf("Initializing memory allocator...\n"); init_allocator(1024); print_tree(free_tree_root, 0); printf("\nAllocating 200 bytes...\n"); void *ptr1 = allocate(200); print_tree(free_tree_root, 0); printf("\nAllocating 300 bytes...\n"); void *ptr2 = allocate(300); print_tree(free_tree_root, 0); printf("\nFreeing first block...\n"); deallocate(ptr1); print_tree(free_tree_root, 0); printf("\nFreeing second block...\n"); deallocate(ptr2); print_tree(free_tree_root, 0); return 0; } ``` 輸出結果如下 ```shell Initializing memory allocator... [1000] Allocating 200 bytes... [776] Allocating 300 bytes... [452] Freeing first block... [452] [200] Freeing second block... [452] [300] [200] ``` #### 延伸問題 2 >參閱 memory-allocators,針對你補完 (及後續改進) 的記憶體配置器,進行效能評比,並解讀其效能表現 ### 測驗 `3` #### 延伸問題 1 >解釋上述程式碼運作原理 〈[Optimized QuickSort — C Implementation (Non-Recursive)](https://alienryderflex.com/quicksort/)〉中提出非遞迴 (non-recursive; iterative) 的快速排序法。使用 L 與 R 分別代表目前要處理子鏈結串列的開頭與結尾節點,再利用 begin[] 作為模擬堆疊的容器,記錄尚待排序的子區段。 題目中程式碼使用了 Linux 風格的 List API 來建立鍊結串列,並使用 value 來紀錄數值,結構如下。 ```c typedef struct __node { long value; struct list_head list; } node_t; ``` 由於每次分割都會產生兩個子列表需要做排序,最壞情況下為每次分割其中一邊只有一個元素,會導致需要分割 n 次,產生 $2 \times n$ 個需要分配的子列表,因此將 begin 所儲存的最大空間設定為 2n ,並且將第一個需要排序的列表設定為 list->next 及把鍊結串列轉為非環狀的結構。 ```c int max_level = 2 * n; struct list_head *begin[max_level]; begin[0] = list->next; list->prev->next = NULL; ``` 接著便開始進行排序的動作, ```graphviz digraph LinkedListSort { rankdir=LR; node [shape=record]; HEAD [label="HEAD", shape=record]; N1 [label="{ <value> 3 }", shape=record]; N2 [label="{ <value> 1 }", shape=record]; N3 [label="{ <value> 4 }", shape=record]; N4 [label="{ <value> 2 }", shape=record]; N5 [label="{ <value> 5 }", shape=record]; HEAD -> N1; N1 -> N2; N2 -> N3; N3 -> N4; N4 -> N5; N5 -> NULL; N2 -> N1 ; N3 -> N2 ; N4 -> N3 ; N5 -> N4 ; } ``` 排序的過程中首先先將 L 及 R 兩指標指向頭及尾。 ```graphviz digraph LinkedListSort { rankdir=LR; node [shape=record]; HEAD [label="HEAD", shape=record]; N3 [label="{ <value> 3 }", shape=record]; N1 [label="{ <value> 1 }", shape=record]; N4 [label="{ <value> 4 }", shape=record]; N2 [label="{ <value> 2 }", shape=record]; N5 [label="{ <value> 5 }", shape=record]; L [label="*L",shape=plaintext]; R [label="*R",shape=plaintext]; L -> N3; R -> N5; HEAD -> N3; N3 -> N1; N1 -> N4; N4 -> N2; N2 -> N5; N5 -> NULL; N5 -> N2 ; N2 -> N4 ; N4 -> N1 ; N1 -> N3 ; } ``` 若 L 及 R 兩者不同,則將 piviot 指向 L 所指的節點,並將數值存於 value。 ```graphviz digraph LinkedListSort { rankdir=LR; node [shape=record]; HEAD [label="HEAD", shape=record]; N3 [label="{ <value> 3 }", shape=record]; N1 [label="{ <value> 1 }", shape=record]; N4 [label="{ <value> 4 }", shape=record]; N2 [label="{ <value> 2 }", shape=record]; N5 [label="{ <value> 5 }", shape=record]; L [label="*L",shape=plaintext]; R [label="*R",shape=plaintext]; piviot [label="*piviot",shape=plaintext]; value [label="value = 3",shape=plaintext]; L -> N3; R -> N5; piviot -> N3 HEAD -> N3; N3 -> N1; N1 -> N4; N4 -> N2; N2 -> N5; N5 -> NULL; N5 -> N2 ; N2 -> N4 ; N4 -> N1 ; N1 -> N3 ; } ``` 接著使用 p 指向 piviot 的下一個節點,並斷開 piviot。 ```graphviz digraph LinkedListSort { rankdir=LR; node [shape=record]; N3 [label="{ <value> 3 }", shape=record]; N1 [label="{ <value> 1 }", shape=record]; N4 [label="{ <value> 4 }", shape=record]; N2 [label="{ <value> 2 }", shape=record]; N5 [label="{ <value> 5 }", shape=record]; L [label="*L",shape=plaintext]; R [label="*R",shape=plaintext]; piviot [label="*piviot",shape=plaintext]; p [label="*p",shape=plaintext]; value [label="value = 3",shape=plaintext]; p -> N1; L -> N3; R -> N5; piviot -> N3 N1 -> N4; N4 -> N2; N2 -> N5; N5 -> NULL; N5 -> N2 ; N2 -> N4 ; N4 -> N1 ; } ``` 使用 p 遍歷節點,將小於 value 的置於 left 中,大於 value 則置於 right 中。 ```c if (n_value > value) { n->next = right; right = n; } else { n->next = left; left = n; } ``` ```graphviz digraph LinkedListSort { rankdir=LR; node [shape=record]; N3 [label="{ <value> 3 }", shape=record]; N1 [label="{ <value> 1 }", shape=record]; N4 [label="{ <value> 4 }", shape=record]; N2 [label="{ <value> 2 }", shape=record]; N5 [label="{ <value> 5 }", shape=record]; L [label="*L",shape=plaintext]; R [label="*R",shape=plaintext]; piviot [label="*piviot",shape=plaintext]; value [label="value = 3",shape=plaintext]; left [label="*left",shape=plaintext]; right [label="*right",shape=plaintext]; piviot -> N3 L -> N3; R -> N5; left -> N2; right -> N5 ; N5 -> N4; N2 -> N1; } ``` 接著將 begin[] 指向對應的位置。 ```c begin[i]= begin[0] = left; begin[i + 1]= begin[1] = pivot; begin[i + 2]= begin[2] = right; left = right = NULL; ``` ```graphviz digraph LinkedListSort { rankdir=LR; node [shape=record]; i [label="i = 2",shape=plaintext]; N3 [label="{ <value> 3 }", shape=record]; N1 [label="{ <value> 1 }", shape=record]; N4 [label="{ <value> 4 }", shape=record]; N2 [label="{ <value> 2 }", shape=record]; N5 [label="{ <value> 5 }", shape=record]; L [label="*L",shape=plaintext]; R [label="*R",shape=plaintext]; begin_i [label="begin[0]",shape=plaintext]; begin_i1 [label="begin[1]",shape=plaintext]; begin_i2 [label="begin[2]",shape=plaintext]; begin_i1 -> N3 L -> N3; R -> N5; begin_i -> N2; begin_i2 -> N5 ; N5 -> N4; N2 -> N1; } ``` 在下一輪中同樣將 L 指向 begin[i] 即為 begin[2] (從較大子序列開始),而 R 則指向 begin[2] 的尾端。 ```graphviz digraph LinkedListSort { rankdir=LR; node [shape=record]; i [label="i = 2",shape=plaintext]; N3 [label="{ <value> 3 }", shape=record]; N1 [label="{ <value> 1 }", shape=record]; N4 [label="{ <value> 4 }", shape=record]; N2 [label="{ <value> 2 }", shape=record]; N5 [label="{ <value> 5 }", shape=record]; L [label="*L",shape=plaintext]; R [label="*R",shape=plaintext]; begin_i [label="begin[0]",shape=plaintext]; begin_i1 [label="begin[1]",shape=plaintext]; begin_i2 [label="begin[2]",shape=plaintext]; begin_i1 -> N3 R -> N4; begin_i -> N2; L -> N5 ; N5 -> N4; begin_i2 -> N5; N2 -> N1; } ``` 此時按照先前的步驟將 piviot 設置在 L 並將 p 指向 piviot 下一個節點 ```graphviz digraph LinkedListSort { rankdir=LR; node [shape=record]; i [label="i = 2",shape=plaintext]; N3 [label="{ <value> 3 }", shape=record]; N1 [label="{ <value> 1 }", shape=record]; N4 [label="{ <value> 4 }", shape=record]; N2 [label="{ <value> 2 }", shape=record]; N5 [label="{ <value> 5 }", shape=record]; value [label="value = 5",shape=plaintext]; piviot [label="*piviot",shape=plaintext]; p [label="*p",shape=plaintext]; begin_i [label="begin[0]",shape=plaintext]; begin_i1 [label="begin[1]",shape=plaintext]; begin_i1 -> N3 begin_i -> N2; N2 -> N1; piviot -> N5; N5 -> N4; p -> N4; } ``` 同樣遍歷節點,將小於 value 的置於 left 中,大於 value 則置於 right 中,並將 begin[] 指向對應的位置。 ```c begin[i]= begin[2] = left; begin[i + 1]= begin[3] = pivot; begin[i + 2]= begin[4] = right; left = right = NULL; ``` ```graphviz digraph LinkedListSort { rankdir=LR; node [shape=record]; i [label="i = 2",shape=plaintext]; N3 [label="{ <value> 3 }", shape=record]; N1 [label="{ <value> 1 }", shape=record]; N4 [label="{ <value> 4 }", shape=record]; N2 [label="{ <value> 2 }", shape=record]; N5 [label="{ <value> 5 }", shape=record]; value [label="value = 5",shape=plaintext]; piviot [label="*piviot",shape=plaintext]; begin_i [label="begin[0]",shape=plaintext]; left [label="*left",shape=plaintext]; right [label="*right",shape=plaintext]; begin_i1 [label="begin[1]",shape=plaintext]; begin_i1 -> N3 begin_i -> N2; N2 -> N1; left -> N4 piviot -> N5; right -> NULL; } ``` ```graphviz digraph LinkedListSort { rankdir=LR; node [shape=record]; i [label="i = 4",shape=plaintext]; N3 [label="{ <value> 3 }", shape=record]; N1 [label="{ <value> 1 }", shape=record]; N4 [label="{ <value> 4 }", shape=record]; N2 [label="{ <value> 2 }", shape=record]; N5 [label="{ <value> 5 }", shape=record]; begin_i [label="begin[0]",shape=plaintext]; begin_i1 [label="begin[1]",shape=plaintext]; begin_i2 [label="begin[2]",shape=plaintext]; begin_i3 [label="begin[3]",shape=plaintext]; begin_i1 -> N3 begin_i -> N2; N2 -> N1; begin_i2 -> N4 begin_i3 -> N5; } ``` 此時將 L 指向 begin[4], R 指向 begin[4] 的尾端,兩者皆指向 NULL,且 L 為 NULL 因此 i= i-1 = 3 。又 3 也同樣為單一一個節點,因此 i 會不斷扣一並在過程中逐步將元素加到 result 中,直到 i = 0 。 ```graphviz digraph LinkedListSort { rankdir=LR; node [shape=record]; i [label="i = 2",shape=plaintext]; N3 [label="{ <value> 3 }", shape=record]; N1 [label="{ <value> 1 }", shape=record]; N4 [label="{ <value> 4 }", shape=record]; N2 [label="{ <value> 2 }", shape=record]; N5 [label="{ <value> 5 }", shape=record]; result [label="*result",shape=plaintext]; begin_i [label="begin[0]",shape=plaintext]; begin_i1 [label="begin[1]",shape=plaintext]; begin_i2 [label="begin[2]",shape=plaintext]; L [label="*L",shape=plaintext]; R [label="*R",shape=plaintext]; begin_i1 -> N3 L -> N2 R -> N1 begin_i -> N2; N2 -> N1; begin_i2 -> N4 result -> N5; } ``` ```graphviz digraph LinkedListSort { rankdir=LR; node [shape=record]; i [label="i = 1",shape=plaintext]; N3 [label="{ <value> 3 }", shape=record]; N1 [label="{ <value> 1 }", shape=record]; N4 [label="{ <value> 4 }", shape=record]; N2 [label="{ <value> 2 }", shape=record]; N5 [label="{ <value> 5 }", shape=record]; result [label="*result",shape=plaintext]; begin_i [label="begin[0]",shape=plaintext]; begin_i1 [label="begin[1]",shape=plaintext]; L [label="*L",shape=plaintext]; R [label="*R",shape=plaintext]; begin_i1 -> N3 L -> N2 R -> N1 begin_i -> N2; N2 -> N1; result -> N4; N4 -> N5 } ``` ```graphviz digraph LinkedListSort { rankdir=LR; node [shape=record]; i [label="i = 0",shape=plaintext]; N3 [label="{ <value> 3 }", shape=record]; N1 [label="{ <value> 1 }", shape=record]; N4 [label="{ <value> 4 }", shape=record]; N2 [label="{ <value> 2 }", shape=record]; N5 [label="{ <value> 5 }", shape=record]; result [label="*result",shape=plaintext]; begin_i [label="begin[0]",shape=plaintext]; L [label="*L",shape=plaintext]; R [label="*R",shape=plaintext]; L -> N2 R -> N1 begin_i -> N2; N2 -> N1; result -> N3; N4 -> N5; N3 -> N4 } ``` 此時再依照先前的方法同樣為 2 及 1 兩個節點設置 begin[]。 ```graphviz digraph LinkedListSort { rankdir=LR; node [shape=record]; i [label="i = 2",shape=plaintext]; N3 [label="{ <value> 3 }", shape=record]; N1 [label="{ <value> 1 }", shape=record]; N4 [label="{ <value> 4 }", shape=record]; N2 [label="{ <value> 2 }", shape=record]; N5 [label="{ <value> 5 }", shape=record]; result [label="*result",shape=plaintext]; begin_i [label="begin[0]",shape=plaintext]; begin_i1 [label="begin[1]",shape=plaintext]; begin_i -> N1; begin_i1 -> N2 result -> N3; N4 -> N5; N3 -> N4 } ``` 最後再使用 result 收回 ```graphviz digraph LinkedListSort { rankdir=LR; node [shape=record]; i [label="i = 1",shape=plaintext]; N3 [label="{ <value> 3 }", shape=record]; N1 [label="{ <value> 1 }", shape=record]; N4 [label="{ <value> 4 }", shape=record]; N2 [label="{ <value> 2 }", shape=record]; N5 [label="{ <value> 5 }", shape=record]; result [label="*result",shape=plaintext]; begin_i [label="begin[0]",shape=plaintext]; begin_i -> N1; result -> N2; N4 -> N5; N3 -> N4 N2 -> N3 } ``` ```graphviz digraph LinkedListSort { rankdir=LR; node [shape=record]; i [label="i = 0",shape=plaintext]; N3 [label="{ <value> 3 }", shape=record]; N1 [label="{ <value> 1 }", shape=record]; N4 [label="{ <value> 4 }", shape=record]; N2 [label="{ <value> 2 }", shape=record]; N5 [label="{ <value> 5 }", shape=record]; result [label="*result",shape=plaintext]; result -> N1; N4 -> N5; N3 -> N4 N2 -> N3 N1 -> N2 } ``` 接著再使用 rebuild_list_link 設置回原本的雙向鍊結串列。 ```graphviz digraph LinkedListSort { rankdir=LR; node [shape=record]; // 定義節點 N1 [label="1 ", shape=record]; N2 [label="2 ", shape=record]; N3 [label="3 ", shape=record]; N4 [label="4 ", shape=record]; N5 [label="5 ", shape=record]; // 指向排序結果 result [label="*result", shape=plaintext]; // 雙向連結:每個節點都指向前後節點 result -> N1; N1 -> N2; N2 -> N3; N3 -> N4; N4 -> N5; N2 -> N1; N3 -> N2; N4-> N3; N5 -> N4; NULL2 [label="NULL", shape=plaintext]; N5 -> NULL2; } ``` #### 延伸問題 2 >研讀〈[A comparative study of linked list sorting algorithms](https://dl.acm.org/doi/pdf/10.1145/225890.225893)〉,分析針對雙向鏈結串列的排序演算法考量,並運用 Linux 核心風格的 List API 予以實作 ## [第二週測驗題](https://hackmd.io/@sysprog/linux2025-quiz2) ### 測驗 `1` #### 延伸問題 1 > 解釋上方程式碼運作原理,改進 random_shuffle_array 也探討 list_for_each_entry_safe 建構的迴圈中,若將 list_move_tail 換為 list_move,為何會無法滿足 stable sorting 首先先確認所使用的結構體,分別儲存鍊結串列的 list_head 及 uint16 數值。 ```c struct listitem { uint16_t i; struct list_head list; }; ``` 接著查看排序的演算法 ```graphviz digraph LinkedListSort { rankdir=LR; node [shape=record]; HEAD [label="HEAD", shape=record]; N3 [label="{ <value> 3 }", shape=record]; N1 [label="{ <value> 1 }", shape=record]; N4 [label="{ <value> 4 }", shape=record]; N2 [label="{ <value> 2 }", shape=record]; N5 [label="{ <value> 5 }", shape=record]; HEAD -> N3; N3 -> N1; N1 -> N4; N4 -> N2; N2 -> N5; N5 -> NULL; N3 -> HEAD N1 -> N3 ; N4 -> N1 ; N2 -> N4 ; N5 -> N2 ; NULL -> N5; } ``` 首先先宣告兩個空的 list_head 分別為 list_less 及 list_greater,將 piviot 設定在整個 list 的開頭並移出 list。 ```graphviz digraph LinkedListSort { rankdir=LR; { rank=same; list_less; list_greater; } HEAD [label="HEAD", shape=record]; N3 [label="<prev>prev| <value> 3 |<next>next", shape=record]; N1 [label="<prev>prev| <value> 1 |<next>next", shape=record]; N4 [label="<prev>prev| <value> 4 |<next>next", shape=record]; N2 [label="<prev>prev| <value> 2 |<next>next", shape=record]; N5 [label="<prev>prev| <value> 5 |<next>next", shape=record]; piviot [label="*piviot",shape=plaintext] piviot -> N3; HEAD -> N1; N1:next -> N4; N4:next -> N2; N2:next -> N5; N5:next -> NULL; N1:prev -> HEAD ; N4:prev -> N1 ; N2:prev -> N4 ; N5:prev -> N2 ; NULL -> N5:value; list_less [label="<prev>prev|*list_less|<next>next", shape=record]; list_greater [label="<prev>prev|list_greater|<next>next", shape=record]; list_less:next -> list_less list_less:prev -> list_less list_greater:next -> list_greater list_greater:prev -> list_greater } ``` 接著使用 list_for_each_entry_safe 遍歷每個節點,將小於 piviot 的放置在 list_less 中,反之則放入 list_greater 中。 ```graphviz digraph LinkedListSort { rankdir=LR; { rank=same; list_less; list_greater; } HEAD [label="HEAD", shape=record]; NULL [label="NULL", shape=record]; N3 [label="<prev>prev| <value> 3 |<next>next", shape=record]; N1 [label="<prev>prev| <value> 1 |<next>next", shape=record]; N4 [label="<prev>prev| <value> 4 |<next>next", shape=record]; N2 [label="<prev>prev| <value> 2 |<next>next", shape=record]; N5 [label="<prev>prev| <value> 5 |<next>next", shape=record]; piviot [label="*piviot",shape=plaintext] piviot -> N3; HEAD -> NULL; list_less [label="<prev>prev|*list_less|<next>next", shape=record]; list_greater [label="<prev>prev|list_greater|<next>next", shape=record]; list_less:next -> N1; N1:prev -> list_less; N1:next -> N2; N2:prev -> N1; list_greater:next -> N4; N4:prev -> list_greater; N4:next -> N5; N5:prev -> N4; } ``` 接著用同樣的方式整理 list_less 及 list_greater,再將 piviot 加回 HEAD 中。 ```graphviz digraph LinkedListSort { rankdir=LR; { rank=same; list_less; list_greater; } HEAD [label="HEAD", shape=record]; NULL [label="NULL", shape=record]; N3 [label="<prev>prev| <value> 3 |<next>next", shape=record]; N1 [label="<prev>prev| <value> 1 |<next>next", shape=record]; N4 [label="<prev>prev| <value> 4 |<next>next", shape=record]; N2 [label="<prev>prev| <value> 2 |<next>next", shape=record]; N5 [label="<prev>prev| <value> 5 |<next>next", shape=record]; HEAD -> N3; N3:prev -> HEAD; N3:next -> NULL; NULL -> N3:value; list_less [label="<prev>prev|*list_less|<next>next", shape=record]; list_greater [label="<prev>prev|list_greater|<next>next", shape=record]; list_less:next -> N1; N1:prev -> list_less; N1:next -> N2; N2:prev -> N1; list_greater:next -> N4; N4:prev -> list_greater; N4:next -> N5; N5:prev -> N4; } ``` 接著再把 list_greater 及 list_less 分別加到鍊結串列的尾端及前端。 ```graphviz digraph LinkedListSort { rankdir=LR; HEAD [label="HEAD", shape=record]; NULL [label="NULL", shape=record]; N3 [label="<prev>prev| <value> 3 |<next>next", shape=record]; N1 [label="<prev>prev| <value> 1 |<next>next", shape=record]; N4 [label="<prev>prev| <value> 4 |<next>next", shape=record]; N2 [label="<prev>prev| <value> 2 |<next>next", shape=record]; N5 [label="<prev>prev| <value> 5 |<next>next", shape=record]; HEAD -> N1; N1:next -> N2; N2:next -> N3; N3:next -> N4; N4:next -> N5; N5:next -> NULL; N1:prev -> HEAD; N5:prev -> N4; N4:prev -> N3; N3:prev -> N2; N2:prev -> N1; } ``` > 探討 list_for_each_entry_safe 建構的迴圈中,若將 list_move_tail 換為 list_move,為何會無法滿足 stable sorting 若有兩相同值的元素如下圖,分別為第一個 4 及第二個 4。 ```graphviz digraph LinkedListSort { rankdir=LR; node [shape=record]; HEAD [label="HEAD", shape=record]; N3 [label="{ <value> 3 }", shape=record]; N1 [label="{ <value> 1 }", shape=record]; N4_1 [label="{ <value> 4 (1) }", shape=record]; N2 [label="{ <value> 2 }", shape=record]; N4_2 [label="{ <value> 4 (2) }", shape=record]; HEAD -> N3; N3 -> N1; N1 -> N4_1; N4_1 -> N2; N2 -> N4_2; N4_2 -> NULL; N3 -> HEAD N1 -> N3 ; N4_1 -> N1 ; N2 -> N4_1 ; N4_2 -> N2 ; NULL -> N5; } ``` 當使用 list_move 時候,由於我們是從前面遍歷至後面,因此第二個 4 會被置於第一個之前 ```graphviz digraph LinkedListSort { rankdir=LR; { rank=same; list_less; list_greater; } HEAD [label="HEAD", shape=record]; NULL [label="NULL", shape=record]; N3 [label="<prev>prev| <value> 3 |<next>next", shape=record]; N1 [label="<prev>prev| <value> 1 |<next>next", shape=record]; N4_1 [label="<prev>prev| <value> 4 (1) |<next>next", shape=record]; N2 [label="<prev>prev| <value> 2 |<next>next", shape=record]; N4_2 [label="<prev>prev| <value> 4 (2) |<next>next", shape=record]; piviot [label="*piviot",shape=plaintext] piviot -> N3; HEAD -> NULL; list_less [label="<prev>prev|*list_less|<next>next", shape=record]; list_greater [label="<prev>prev|list_greater|<next>next", shape=record]; list_less:next -> N1; N1:prev -> list_less; N1:next -> N2; N2:prev -> N1; list_greater:next -> N4_2; N4_2:prev -> list_greater; N4_2:next -> N4_1; N4_1:prev -> N4_2; } ``` 此時重複呼叫 quick_sort 並不會將 list_great 中的 4 重新排序,因此會造成兩個的位置互換,非 stable sort。 ```graphviz digraph LinkedListSort { rankdir=LR; HEAD [label="HEAD", shape=record]; NULL [label="NULL", shape=record]; N3 [label="<prev>prev| <value> 3 |<next>next", shape=record]; N1 [label="<prev>prev| <value> 1 |<next>next", shape=record]; N4_1 [label="<prev>prev| <value> 4 (1) |<next>next", shape=record]; N2 [label="<prev>prev| <value> 2 |<next>next", shape=record]; N4_2 [label="<prev>prev| <value> 4 (2) |<next>next", shape=record]; HEAD -> N1; N1:next -> N2; N2:next -> N3; N3:next -> N4_2; N4_2:next -> N4_1; N4_1:next -> NULL; N1:prev -> HEAD; N4_1:prev -> N4_2; N4_2:prev -> N3; N3:prev -> N2; N2:prev -> N1; } ``` 改進 random_shuffle_array #### 延伸問題 2 > 將程式碼整合進 lab0 提及的 qtest,並探討環狀雙向鏈結串列的排序效能,並善用 perf 一類的效能分析工具,探討包含 Linux 核心 list_sort 在內排序實作的效能表現並充分解釋 ### 測驗 `2` #### 延伸問題 1 > 解釋上述程式碼,並探討擴充為 $\lceil \sqrt{x}) \rceil$ (向上取整數) 實作,該如何調整並保持只用整數加減及位元操作 先確認 counting leading zero 的方法, ```c= #define clz32(x) clz2(x, 0) static const int mask[] = {0, 8, 12, 14}; static const int magic[] = {2, 1, 0, 0}; 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); } ``` 8、9行首先先確認輸入,若輸入為 0 則表示 32 位元中全部皆是 0 ,因此回傳 32 。 接著將輸入的 x 分割成前半部及後半部 前半部直接將 x 右移完成。以 `0b0001 0010 0101 0011` 取前半部為例,由於這邊僅剩下 16 位數因此可以推得 c = 1,需要位移 8 位。透過將 `16 >> c` (即除以 c 次 2)來設定需要右移的數量,便可以取得 `upper = 0b0001 0010`。 後半部則使用 `0xFFFF >> mask` 來設定對應的遮罩,不同 c 所需要的遮罩分別如下 | c | mask | | -------- | -------- | | 0 | 0b 1111 1111 1111 1111 | | 1 | 0b 0000 0000 1111 1111 | | 2 | 0b 0000 0000 0000 1111 | | 3 | 0b 0000 0000 0000 0011 | 因此設定的遮罩為 `mask[] = {0, 8, 12, 14};`。透過遮罩便可以把對應的,同樣以 `0b0001 0010 0101 0011` 取後半部為例,與 `0b0000 0000 1111 1111` 取 AND 後便為後半部`0b0000 0000 0101 0011`。 若 c = 3 則表示目前僅剩下兩位數,因此直接使用查表的方式來確認 clz。 | bits | Clz | | -------- | -------- | | 00 | 2 | | 01 | 1 | | 10 | 0 | | 11 | 0 | 接著便迭代呼叫 `clz2`,若前半位大於 0 則僅須計算前半的 leading zero 即為整個 x 的 leading zero。若前半為皆為 0 則計算後半部的 leading zero 並加上前半位 0 的數量。 ```c return upper ? clz2(upper, c + 1) : (16 >> (c)) + clz2(lower, c + 1); ``` 若要計算 64 位元,則確認前半位是否全為 0 ,若全為 0 則直接計算後半部的部份再加上 32 即可,若為非則直接計算前半部即可。 ```c static inline int clz64(uint64_t x) { return (x >> 32) ? clz32((uint32_t)(x >> 32)) : clz32((uint32_t)x) + 32; } ``` 演算法使用逐步逼近的方式來計算平方根,首先先計算 m 的起始值,其中最困惑的便是 m 起始值的部份。 ```c int shift = (total_bits - 1 - clz64(x)) & ~1; m = 1ULL << shift; ``` 其中 `total_bits - 1 - clz64(x)` 表示最高位 1 的位置,接著再透過與 `~1` 做 AND 運算來清除最低位以確保位移值是偶數,接著將 m 設置在最高位作為估計的起始。 在逐步檢視程式碼如何逼近平方根前,先參考[從 √2 的存在談開平方根的快速運算](https://hackmd.io/@sysprog/sqrt#Digit-by-digit-Method)理解其數學推導。 實際以 $10^2 = (7+2+1)^2 = 100$ 為範例,首先計算 $P_i$ 如下 $$ \begin{align} P_0 &= N = 10 \\ P_1 &= P_0 - a_0 = 10 - 1 = 9 \\ P_2 &= P_1 - a_1 = 10 - 2 = 7 \end{align} $$ 接著從高位開始計算 $Y_m$ $$ \begin{align} Y_2 &= a_2 ^ 2 = 49 \\ Y_1 &= (2 \times P_2 + a_1)\times a_1 = (2 \times 7 + 2) \times 2 = 32 \\ Y_0 &= (2 \times P_1 + a_0)\times a_0 = (2 \times 1 + 1) \times 1 = 19 \end{align} $$ 因此在計算的過程中會先計算高位的平方,再逐次加入 $Y_m$ 最後計算總合為 $$ 49+32+19 = 100 $$ 而二進制可以將一般化的平方和表示為 $$ \begin{align} N^2 &\approx (b_n\times 2^0 + b_{n-1}\times 2^{1}+\dots b_ 1\times2^{n-1} +b_0\times2^n)^2 \\ &= (a_n + a_{n-1} + \dots a_1 + a_0)^2 \end{align} $$ 其中 $b_i\in\lbrace 0,1\rbrace,\text{ for }i=0,1\dots,n$ 且 $P_m=P_{m+1}+a_{m}=P_{m+1}+b_{m}\times 2^{m}$ 因此我們可以利用最高位 1 來有個初始值,再藉由不斷加上新的低位元數值來逼近結果 。 回頭檢視程式碼, 我們從最高位的 1 開始計算並使用 `b` 來紀錄目前要測試的數值,藉由測試 `y + m` 是否會超過 x 來判斷能不能更逼近平方根。若 `x >= b` 則表示測試的 b 為可行的解,因此更新 x 及 y。但此處仍在理解為何需要將 y 及 m 位移。 #### 延伸問題 2 >參照[計算機結構測驗題](https://hackmd.io/@sysprog/arch2024-quiz3-sol#Problem-C) 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 語言實作,並比較不同手法的整數開平方根效能 ### 測驗 `3` #### 延伸問題 1 >解釋上述程式碼運作原理,應提供測試程式碼 使用 Hash table 的方式解決紀錄缺少的程式碼,參考測驗題中的範例 > nums = `[2, 11, 7, 15]` target = 9 步驟如下: 1. `nums[0] = 2`,且 2 沒有對應的 HT[2],因此建立 HT[9-2] = 0。 2. `nums[1] = 11` 且 11 沒有對應的 HT[11],因此建立 HT[9-11] = 1。 3. `nums[2] = 7` 對應的 HT[7] = 0 存在表示 7 和 nums[0] 相加為 9 ,因此回傳 `[2, HT[7]] = [2, 0]`。 查看測驗題中的實作,首先先定義結構體。透過 `hlist_head` 定義 hash table 的頭並使用 `hlist_node` 來串接每個 `hash_key`。 ```graphviz digraph HashTable { rankdir=LR; node [shape=record]; HEAD [label="|<list_head>list_head.first|||", shape=record]; N1 [label="{hash_key}|key|*data|{<pprev>**pprev|<next>*next}", shape=record]; N2 [label="{hash_key}|key|*data|{<pprev>**pprev|<next>*next}", shape=record]; N3 [label="{hash_key}|key|*data|{<pprev>**pprev|<next>*next}", shape=record]; HEAD:list_head -> N1:w; N1:next -> N2:w; N2:next -> N3:w; N1:pprev:s -> HEAD:list_head:se [color=gray]; N2:pprev:s -> N1:next:s [color=gray]; N3:pprev:s -> N2:next:s [color=gray]; N3:next -> NULL; } ``` ```c struct hlist_head { struct hlist_node *first; }; struct hlist_node { struct hlist_node *next, **pprev; }; typedef struct { int bits; struct hlist_head *ht; } map_t; struct hash_key { int key; void *data; struct hlist_node node; }; ``` 接著程式中先用 `map_init` 初始化 hash table ,其中 bits 儲存需要分配的大小,並使用 marco 將 bits 轉換為對應的數值。以 bits = 10 為例,傳入後轉換為 1024 因此將 hash table 上限為 1024 個值。 ```c #define MAP_HASH_SIZE(bits) (1U << (bits)) ``` 傳換後再由 malloc 分配對應的空間至 ht 內 ```c map->ht = malloc(sizeof(struct hlist_head) * MAP_HASH_SIZE(map->bits)); ``` 接著遍歷 nums 並使用 map_get 來確認 map 中是否有對應的 key ,若存在則回傳結果,若不存在則使用 map_add 加入至 hash table 中。 ```c for (int i = 0; i < numsSize; i++) { int *p = map_get(map, target - nums[i]); if (p) { /* found */ ret[0] = i, ret[1] = *p; *returnSize = 2; break; } p = malloc(sizeof(int)); *p = i; map_add(map, nums[i], p); } ``` 先查看雜湊函數的部份 ```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); } ``` TBD 使用 map_get 取得對應的 key ,其中用到 find_key 來找到對應的 key 位置。 ```c void *map_get(map_t *map, int key) { struct hash_key *kn = find_key(map, key); return kn ? kn->data : NULL; } ``` find_key 先使用 hash 來找到對應鍊結串列的頭 ```c struct hlist_head *head = &(map->ht)[hash(key, map->bits)]; ``` 接著再遍歷鍊結串列來找到對應的位址 ```c for (struct hlist_node *p = head->first; p; p = p->next) { struct hash_key *kn = container_of(p, struct hash_key, node); if (kn->key == key) return kn; } ``` 當查找不到對應的 key 時,便需要使用 map_add 將目前的值插入到 hash table 中 首先先確認 key 是否在 hash table 中,若已存在則返回。 ```c struct hash_key *kn = find_key(map, key); if (kn) return; ``` 接著設定新的節點並取得要插入的鍊結串列的頭節點 ```graphviz digraph HashTable { rankdir=LR; node [shape=record]; HEAD [label="|<list_head>list_head|||", shape=record]; N1 [label="{hash_key}|key|*data|{<pprev>**pprev|<next>*next}", shape=record]; N2 [label="{hash_key}|key|*data|{<pprev>**pprev|<next>*next}", shape=record]; N3 [label="{hash_key}|key|*data|{<pprev>**pprev|<next>*next}", shape=record]; N4 [label="{NEW_hash_key}|key|*data|{<pprev>**pprev|<next>*next}", shape=record]; h [label="*h", shape=plaintext]; HEAD:list_head -> N1:w; N1:next -> N2:w; N2:next -> N3:w; h-> HEAD:list_head; N1:pprev:s -> HEAD:list_head:se [color=gray]; N2:pprev:s -> N1:next:s [color=gray]; N3:pprev:s -> N2:next:s [color=gray]; N3:next -> NULL; } ``` ```c kn = malloc(sizeof(struct hash_key)); kn->key = key, kn->data = data; struct hlist_head *h = &map->ht[hash(key, map->bits)]; ``` 再將設定好的新節點插入到鍊結串列的前端 ```graphviz digraph HashTable { rankdir=LR; node [shape=record]; HEAD [label="|<list_head>list_head|||", shape=record]; N1 [label="{hash_key}|key|*data|{<pprev>**pprev|<next>*next}", shape=record]; N2 [label="{hash_key}|key|*data|{<pprev>**pprev|<next>*next}", shape=record]; N3 [label="{hash_key}|key|*data|{<pprev>**pprev|<next>*next}", shape=record]; N4 [label="{NEW_hash_key}|key|*data|{<pprev>**pprev|<next>*next}", shape=record]; h [label="*h", shape=plaintext]; first [label="*first", shape=plaintext]; n [label="*n", shape=plaintext]; n -> N4; first -> N1:n HEAD:list_head -> N1:w; N1:next -> N2:w; N2:next -> N3:w; h-> HEAD:list_head; N1:pprev:s -> HEAD:list_head:se [color=gray]; N2:pprev:s -> N1:next:s [color=gray]; N3:pprev:s -> N2:next:s [color=gray]; N3:next -> NULL; } ``` ```graphviz digraph HashTable { rankdir=LR; node [shape=record]; HEAD [label="|<list_head>list_head|||", shape=record]; N1 [label="{hash_key}|key|*data|{<pprev>**pprev|<next>*next}", shape=record]; N2 [label="{hash_key}|key|*data|{<pprev>**pprev|<next>*next}", shape=record]; N3 [label="{hash_key}|key|*data|{<pprev>**pprev|<next>*next}", shape=record]; N4 [label="{NEW_hash_key}|key|*data|{<pprev>**pprev|<next>*next}", shape=record]; h [label="*h", shape=plaintext]; first [label="*first", shape=plaintext]; first -> N4:n N1:next -> N2:w; N2:next -> N3:w; h-> HEAD:list_head; N2:pprev:s -> N1:next:s [color=gray]; N3:pprev:s -> N2:next:s [color=gray]; N3:next -> NULL; HEAD:list_head -> N4; N4:pprev -> HEAD:list_head:es [color=gray]; N4:next -> N1:w; N1:pprev:s -> N4:next:s [color=gray]; } ``` ```c struct hlist_node *n = &kn->node, *first = h->first; n->next = first; if (first) first->pprev = &n->next; h->first = n; n->pprev = &h->first; ``` #### 延伸問題 2 >進行《[The Art of Computer Programming](https://www-cs-faculty.stanford.edu/~knuth/taocp.html)》(vol 3) 一書section 6.4 的 exercise 8,也就是證明 Theorem S #### 延伸問題 3 >探討 Linux 核心中使用 hash table 的應用案例,至少二項,應當列出原始程式碼並分析,斟酌搭配 git log 以得知 Linux 核心開發者變更程式碼的考量因素