contributed by <Nsly0204
>
解釋程式碼運作原理
先分析以下程式碼,可以發現 list_item
是一個類似 linux 核心風格的鍊結串列實作,list_item
類似於 list_head
,而list_t
類似於 *head
。
有了以上分析再結合指標的指標的概念,將 p 作為指向 (*node)->next
的指標,而 (*p) 指向 node ,由此可知當*p != before
時持續往下走訪,直到停止條件 (*p)->next == before
時,把 node 替換成 item 即為所求。
在執行以下程式時出現 segmentation fault:
利用 gdb 偵錯之後發現在第7行時發現錯誤
Program received signal SIGSEGV, Segmentation fault.
0x0000555555555159 in main () at dbl_ptr_test.c:21
21 it1->value = 1;
查閱 c11 規格書
§ 6.2.4 Storage durations of objects
A non-lvalue expression with structure or union type, where the structure or union
contains a member with array type (including, recursively, members of all contained
structures and unions) refers to an object with automatic storage duration and temporary
lifetime.36) Its lifetime begins when the expression is evaluated and its initial value is the
value of the expression. Its lifetime ends when the evaluation of the containing full
expression or full declarator ends. Any attempt to modify an object with temporary
lifetime results in undefined behavior.
思考後發現來目前所有指標變數 it1
,it2
等都是只有 temporary lifetime 的指標,尚未被配置記憶體位置,觸發上述規格書末段的 undefined behavior ,須補上顯示記憶體配置即可正常執行。
在原本的quiz1中不用這樣寫的原因是 static list_t l;
為 file scope variable ,代表在編譯時在 .data 的部份就有靜態配置記憶體,故沒有發生 undefined behavier。
在現有的程式碼基礎上,撰寫合併排序操作
快速複習BST(Binary Search Tree)
解釋上方程式碼運作的原理,並嘗試補完程式碼,使其可獨立執行,應提供相關的測試程式碼
本次測驗嘗試用樹狀結構模擬記憶體管理,二元搜索樹(以下簡稱BST)的節點代表可用空間,函式 remove_free_tree 代表目前需要一個 target->size
大小的記憶體區塊,對應操作從 BST 中找到一個大於等於要求 (target) 的記憶體空間進行分配,被分配的空間會從管理閒置空間的 BST 中被刪除。
BST 的刪除通常會被分成三種情況:
3 的情況因為需要把左右子數接回,我們需要找到 inorder predecessor 也就是左子樹中最右邊的點(小於root中最大的節點)取代被移除的節點所以我們進一步討論子節點取代被刪除節點的可能。
find_free_tree
考慮到如果目的是管理樹狀結構的記憶體區塊,樹狀結構本身會很深,外加 void remove_free_tree(block_t **root, block_t *target)
在每次遞迴都會呼叫此函式,若使用常規的遞迴寫法可能會有 stack overflow 發生,故這裡使用迭代實現。
測試
這裡參考 rota1001 補上 insert_tree 和主函式,以亂序插入10個節點測試程式運作。
void insert_tree(block_t **root, size_t size)
int main ()
範例輸出(依序刪除0~9的節點):
參閱 memory-allocators,針對你補完 (及後續改進) 的記憶體配置器,進行效能評比,並解讀其效能表現
解釋上述程式碼運作原理
研讀〈A comparative study of linked list sorting algorithms〉,分析針對雙向鏈結串列的排序演算法考量,並運用 Linux 核心風格的 List API 予以實作
寫過 lab0 之後本次小考相對輕鬆,程式碼如下,引入 lab0 中的 list.h
即可編譯執行。
解釋上方程式碼運作原理,改進 random_shuffle_array 也探討 list_for_each_entry_safe 建構的迴圈中,若將 list_move_tail 換為 list_move,為何會無法滿足 stable sorting
Lehmer Random Number Generator(Lehmer RNG)
本次小考呼叫兩次 8 bits Lehmer RNG 合併實作出一個 16 bits 亂數,參考維基百科,其基本思想為:
可以發現一個 Lehmer RNG 需要一個起始值 , 倍數 , 和同餘數 ,其中 互質顯然為好的選擇,但因為用 mod 實作,我們可以預期此亂數產生器有以下缺陷:
結論來說, 的選擇在 Lehmer RNG 中尤其重要,通常會強制選擇一個很大的質數,如 或是 ,而本次小考利用三個 Lehmer RNG 互相做位元運算即是減少尾數循環週期短的的經典手段,而把高低 8 bits 分開產生進一步延長總體的循環週期,更多的數學分析考以參考 linear congruential generator。
若將 list_move_tail
換為 list_move
,為何會無法滿足 stable sorting
答案很簡單,在我們 top-down 遞迴拆分鏈結串列時默認元素排序是由左而右,其中 list_move
是從 head
插入,排序因此變成由右往左,無法滿足 stable sorting。
參考 lab0 參考使用 Fisher-Yates shuffle algorithm 實作達成:
即 unbiased shuffle。
可以進一步學習 rota1001 使用 bit operator 的精簡 swap。
可以先參考從 √2 的存在談開平方根的快速運算 和 檢討第二週測驗題,了解 Digit-by-digit 方法如何只使用加減與位移運算來開平方根。
以下將用兩部分說明實作:
Digit-by-digit 的實作可進一步分成以下步驟:
Count leading zeros 實作: clz2()
根據題目敘述,在不斷把 x
拆分上下半部到每個半部只剩下兩位時,我們會直接用 magic[]
進行查表。
需要輸入迭代次數 c
的原因為,總共輸入有 32 bits ,在第三次遞迴呼叫,也就是第四次執行時, x 有 32/8 = 4 bits ,也就是 upper
和 lower
個剩下 2bits 時進行查表操作 return upper ? magic[upper] : 2 + magic[lower];
,由此我們也知道 magic[index]
對應的值就是在最後 2 bits (index)下,對應的 0 個數 。
至此我們清楚知道 uint32_t upper = (x >> (16 >> c));
是藉由 16 除以 調整位移次數,但至於低位部分需要的 mask 需要不一樣的位移次數,參考下表可以簡單知道在不同迭代次數為了達成所需要 1 的個數,我們如何設計 mask[]
。
c | x bits | mask |
---|---|---|
0 | 32 | 0b 1111 1111 1111 1111 |
1 | 16 | 0b 0000 0000 1111 1111 |
2 | 8 | 0b 0000 0000 0000 1111 |
3 | 4 | 0b 0000 0000 0000 0011 |
Digit-by-digit 實作: sqrti()
為什麼需要 int shift = (total_bits - 1 - clz64(x)) & ~1;
一開始看這個程式碼的時候我不太理解為什麼 m 一定要位移偶數次,直到我參考 rota1001 的說明我才恍然大悟,原來真正的原因是我們發現 的 first set one 一定出現在 N 的 first set one 一半的地方,如果 的 first set one 不能被 2 整除,則無條件進位,等效於 shift
無條件捨去,可以參考 rota1001 大大所寫的 for(int i = (shift / 2); i >= 0; i--)
,覺得不直觀的也可以直接進行二進位乘法進行驗證。
接下來的部分相對直觀,我們首先定義:
p
代表 ,等價直到現在迭代次數 i 為止的 ,注意到 從 n 遞減, 。b
代表 ,代表下一次迭代若增加一 bit 1 會讓 增加多少。x
代表 ,代表計算道第 i 位的誤差, i 並非是迭代次數。總結來說我們需要藉由比較計算誤差 和 的大小去決定我們的答案 的下一位 該是 0 或是 ,若下一位是 0, remain error 不變,下一位是 1 ,則其計算方法如下:
第二部分
這裡已經透過去比較 ,巧妙的避開 平方的問題了,但為了近一步減少位移運算,我們發現 p
重複出現,透過代數代換 ,y = (p << (i + 1))
得到:
b = (p << (i + 1)) + (1 << (2 * i));
替換成 b = y + (1 << (2 * i));
默認 。p += (1 << i)
替換 y += (y >> 1) + (1 << (2 * i))
。第二步的替換比較不直觀,本來以為需要複雜的數學推導,實際做過之後發現只是單純的把代數代換展開:
y = (p << (i + 1))
,這邊讓容易搞混的地方是 是上一次迭代的值,這邊的 指的是做完本次迭代的 值,故在執行到 15 行之前都只有 。y += (y >> 1) + (1 << (2 * i))
,至此完成計算到第 位的整數該根號運算,答案為 。從整數平方根到浮點數開平方根:導讀 Python's Square Root Algorithm。
本次測驗一樣填完答案後引入 lab0 的 list.h
即可編譯執行。
參考以上實作, linux 核心風格的實作特色在於善用其獨特的 container_of
函式之外,就是善用 pointer 和 indirect pointer。因此以下會著重對這兩部份進行討論。
由於 hash table 的實作只需要單向鏈結串列,因此反向的 prev
指標可以被我們善加利用。考慮我們在刪除單向鏈結串列節點時,一般的實作方法會需要一個額外的 pointer 去指向被刪除的節點或是被刪除的下一個節點
先思考一般的單向鏈結串列刪除特定節點會遇到的例外問題:
list_head
則不刪除。因此,我們把閒置的 *prev
指標改成間接指標 **pprev
直接存取上一個 node 所在的位址(實際上為上一個節點指向下一個節點的指標next
)。
p
為走訪指標,n
指向 *p
的上一個節點,程式會逐步從頭走訪單向鏈結串列,走訪中止條件為 p=NULL
,刪除時我們全程只需要對 n
進行操作,先用變數紀錄兩個指標 n->next
和 n->pprev
,因為 pprev
指標存在的關係,我們只須判斷是否存在下一個需要接上的指標後即可 free 掉節點 n
,如此我們發現: 無論 n
是否是指向 NULL ,因為我們不需要對節點 n
本身進行操作 (只需要存下 *pprev
的指標,不需要依賴 n->next
),故可以直接對節點 n 進行刪除,達成程式碼的簡潔。
我們必須先了解以下兩者的差別:
pointer_type *
就能看出是指標,而後面的 [array_size]
則代表有 array_size 這麼多個指標。pointer_type array_name[array_size]
的基本陣列宣告後比較可以知道。這裡將 array_name
轉型成為指標,意思是說 array_name 從原本的陣列變成一個指向陣列的指標。functionPtr
轉形成指標的形式。接下來參考論壇討論 Declare a C/C++ function returning pointer to array of integer pointers 根據裡面提供的程式並在 godbolt 驗證。
有了以上的知識我們觀察解答然後逐一拆解