# 2019q1 Homework1 (list) contributed by < `Zames-Chang` > * [F02: list](https://hackmd.io/s/S12jCWKHN) ## 實作出 merge sort * 附上你的修改過程,也需要讓 qtest 得以支援 * 可將 dict 裡頭的測試資料拿來作效能分析的輸入 * 思考提升排序效率的方法,需要做平均和最壞狀況的分析 ## qtest支援 在qtest command指令中加入sort指令 ```c= static void console_init(){ ... add_cmd("sort", do_sort, " | Merge sort queue contents"); ... } bool do_sort(int argc, char *argv[]){ if (argc != 1) { report(1, "%s takes no arguments", argv[0]); return false; } error_check(); set_noallocate_mode(true); if (exception_setup(true)) my_merge_sort(q); // implement merge sort in this function exception_cancel(); set_noallocate_mode(false); show_queue(3); return !error_check(); } ``` ### 自動化單元測試 新增trace-16-mysort.cmd 因為qtest中的queue的元素為string,為了測試排序的功能,我假設輸入都是一個char,並根據他們的ascII code做排序。 ``` new ih a ih c ih b ih e ih d ih h ih h sort show ``` 並在driver.py加入 ```python traceDict = { ... 16 : "trace-16-mysort" } traceProbs = { ... 16 : "Trace-16" } ``` #### result ``` +++ TESTING trace trace-16-mysort: q = [a b c d e h h] ``` ### Merge Sort ```c= /** * merge_sort - using merge sort to sort queue * @head: pointer to queue first element * * queue element must be a single char * @Return :pointer to sorted link list head; */ list_ele_t* merge_sort(list_ele_t* head){ if(head && head->next){ list_ele_t* mid = spice_half(head); head = merge_sort(head); mid = merge_sort(mid); list_ele_t* result = sort_merge(head,mid); return result; } else{ return head; } } ``` ### spice_half ```c= /** * spice_half - cut link list into half * @head: pointer to queue first element * * @Return: half of remaining link list * e.g. a->b->c->d splice into a->b,c->d and return pointer to c */ list_ele_t* spice_half(list_ele_t* head){ if(head){ list_ele_t *mid = get_mid(head); list_ele_t *next_start = mid->next; mid->next = NULL; return next_start; } else return head; } ``` ### get_mid ```c= /** * get_mid - get mid element of link list * @head: pointer to queue first element * * @Return: pointer to mid of link list */ list_ele_t* get_mid(list_ele_t* head){ int i = 0; list_ele_t* des = head; while(head){ head = head->next; i++; } for(int j=0;j<i/2 - 1;j++) des = des->next; return des; } ``` ### sort_merge ```c= /** * insert_tail - insert node_2 in tail of node_1 * @node_1: pointer element * @node_1: pointer element * */ void insert_tail(list_ele_t *node_1,list_ele_t* node_2){ if(node_1 && node_2){ list_ele_t* temp = node_1->next; node_1->next = node_2; node_2->next = temp; } } /** * sort_merge - merge two sorted link list * @head1: pointer to first sorted link list * @head2: pointer to second sorted link list * * @Return: pointer to merged link list head */ list_ele_t* sort_merge(list_ele_t* head1,list_ele_t* head2){ if(head1 && head2){ if(*(head1->value) > *(head2->value)){ list_ele_t* tmp = head2; head2 = head1; head1 = tmp; } } else{ return head1 ? head1 : head2; } list_ele_t* org_head = head1; while(head2){ if(head1){ if(head1->next == NULL){ head1->next = head2; break; } else if(*(head1->value) < *(head2->value) && *(head1->next->value) > *(head2->value)){ list_ele_t* item = head2; head2 = head2->next; insert_tail(head1,item); } else{ head1 = head1->next; } } } return org_head; } ``` ## 自我檢查事項 ### 為何 Linux 採用 macro 來實作 linked list?一般的 function call 有何成本? macro 在編譯前會被 preprocesser 把對應的程式碼貼到macro的地方,而不是一個 function call 的形式。 一個 function call 很麻煩,要把當前暫存器的東西備份一份到暫存器或記憶體,分配空間給 local 變數, Stack 要長上去,中間牽涉到許多記憶體的變化。耗時又費工。 ### Linux 應用 linked list 在哪些場合?舉三個案例並附上對應程式碼,需要解說,以及揣摩對應的考量 `linux/list.h`中的double link list ```c= struct list_head { struct list_head *next, *prev; }; struct My_DATA { int Data_A; int Data_B; int Data_C; struct list_head list; }; ``` 大概結構會像這樣 ![](https://i.imgur.com/hbYNlv4.png) (參考來自:https://myao0730.blogspot.com/2016/12/linux.html) 讓我比較疑惑的是,<s>一般來說</s> doubly linked list不是都是 :::danger 不要濫用「一般來說」這詞,你到底看了多少軟體設計和實作,才讓你得以「總結」出軟體開發的規律,才有「一般」可言呢? 另外,注意術語寫為 **doubly** linked list,從細節做好,快去改! :notes: jserv ::: ```clike struct Node { struct Node *next, *prev; int val; }; ``` 為何它要特別定義一個 list_head裝下兩個 pointer,更重要的是就算我指到 next的 node我也拿不到資料阿?因為指到下一個 list_head中也沒有 Data_A,Data_B,Data_C阿?這邊我猜測應該跟 strcut記憶體是連續的有關吧?也許它可以透過相對位子取得 Data_A,Data_B,Data_C? 後來我看到查到[這篇文章](http://adrianhuang.blogspot.com/2007/10/linux-kernel-listhead.html) :::danger 上方連結有標題,為何不清楚書寫呢? :notes: jserv ::: ```clike struct student { char name[16]; int id; struct list_head list; }; ``` ![](https://i.imgur.com/T2fS3hc.png) 證實了應該我的想法,所以假設我要取的 student的 name ```c= struct list_head list next = head->next; char *temp; temp = (char*) (((void*) &next )- 20); ``` 在 linux中process的資訊是由 task_struct管理,譬如說 `struct list_head children`,就是這個 process中的有沒有子 process,如果有會以 link list串接在這裡。 `struct list_head sibling`,則是存跟當前這個 process同樣 parent的進程 ```c= struct task_struct { ... struct task_struct __rcu *real_parent; struct task_struct __rcu *parent; struct list_head children; struct list_head sibling; ... } ``` ### GNU extension 的 typeof 有何作用?在程式碼中扮演什麼角色? typeof的功能是去回傳一個跟帶入的 instance同樣型別的type, ,它會在你編譯前的預處理時去幫你把 typeof的內容轉換成對應的型別 <s>e.x.</s> :::danger "for example" 縮寫是 [e.g.](https://www.merriam-webster.com/dictionary/e.g.),不是 "ex" :notes: jserv ::: ```clike int a; typeof(a) b; // = int b; typeof(&a) c; // = int* c; ``` after preprocess ```clike int a; int b; int* c; ``` 在linux程式碼中,它需要在 complie的時候先去確認某一些變數是不是型別是一樣的,譬如說以下 macro就是用於確認某個instance是否為某個特定型態。 ```clike /* * Check at compile time that something is of a particular type. * Always evaluates to 1 so you may use it easily in comparisons. */ #define typecheck(type,x) \ ({ type __dummy; \ typeof(x) __dummy2; \ (void)(&__dummy == &__dummy2); \ 1; \ }) ``` 另一個例子是當你希望把兩個 instance的型別作對調的時候,你不太可能在一開使用的時候就知道兩個變數分別的型別,這時候就可以利用typeof來得到兩個人分別的型別,並且對調 ```clike /* * swap - swap value of @a and @b */ #define swap(a, b) \ do { typeof(a) __tmp = (a); (a) = (b); (b) = __tmp; } while (0) ``` [reference](http://deltamaster.is-programmer.com/posts/37253.html) ### 解釋以下巨集的原理 ```clike #define container_of(ptr, type, member) \ __extension__({ \ const __typeof__(((type *) 0)->member) *__pmember = (ptr); \ (type *) ((char *) __pmember - offsetof(type, member)); \ }) ``` `const __typeof__(((type *) 0)->member) *__pmember = (ptr)` 這段是在製造一個 member的指針,並指向 ptr的位址,我這邊比較不理解的地方是為啥不用`const __typeof__(member) __pmember = (ptr)`要大費周章的先用轉成比較上層的struct在去存取下面的member `(type *) ((char *) __pmember - offsetof(type, member))` 這行則是透過減掉 member在此 struct中的偏移量,進而得到這個struct的起始位址。 ### 除了你熟悉的 add 和 delete 操作,list.h 還定義一系列操作,為什麼呢?這些有什麼益處? 讓使用者可以用更高level的角度來操作 link list,不然的話像原始碼中它如何判斷 link list是到底了呢?一般來講我們自己寫程式的話會判斷它最後一個元素有沒有指到 null,但是linux不是這樣實做,它實做的方式是去比較`tail->next == head`,因此不了解 linux link list實做細節的話,會很容易踩到坑 :::danger 不要 ==憑感覺== 書寫,應該搭配實際程式碼和自行撰寫的分析數據來探討。及早脫離「文組^TM^」。特別在你撰寫過 merge sort 一類的程式後,針對 linked list 的例外處理就該有所察覺。 :notes: jserv ::: ### linked list 採用環狀是基於哪些考量? 比較彈性,在 init時候它會把 tail跟 head都指向 head,因為假設他不是環狀的話你要insert tail就必須要走到最後一個元素才能insert也就需要`O(n)`的時間複雜度,然而如果是環狀的話只要`head->pre`就是tail了,這樣只要`O(1)`。而且假設搜尋link list的某個元素,而且你知道從尾巴往前找比從頭往後找要快的話,環狀的結構就可以滿足這個需求,在linux中這個macro叫做`list_for_each_entry_reverse` ### 什麼情境會需要對 linked list 做排序呢?舉出真實世界的應用,最好在作業系統核心出現 例如說`$ htop`,由於程式會隨著時間加入或刪除,你不太可能用一個 array去 maintain,但是你又常常需要根據 cpu的使用時間去做排序,因此這個時候我們就必須要要對 link list做排序。 :::danger 請把話說好,難道 "maintain" 是關鍵字嗎?這非得用英文寫嗎?程式寫不好,真的不可恥,但是連話都說不好,如何跟其他 (專業的) 工程人員溝通呢? :notes: jserv ::: ### 什麼情境會需要需要找到 第 k 大/小元素 呢?你打算如何實作? 譬如說你要在作業系統中找到前三大的使用cpu的程式。 我會使用 merge sort 先把 link list 排序好,再取第k個element出來,這樣所需要的時間複雜度為`O(nlogn)` :::danger Show me the code! 從 Linux 核心找出實際應用的程式碼出來。 :notes: jserv ::: ### list_for_each_safe 和 list_for_each 的差異在哪?“safe” 在執行時期的影響為何? list_for_each 假設你在執行的過程中不會有節點被刪掉,但是safe的版本會幫你檢查這件事情有沒有被發生 ### for_each 風格的開發方式對程式開發者的影響為何?提示:對照其他程式語言,如 Perl 和 Python for_each 這種開發風格可以讓程式發開者不需要事先知道list的長度,也不會因為改動for loop中的i造成confuse的問題。 e.x. ```c= for(int i=0;i<5;i++){ array[i] = 0; i--; } ``` ### 程式註解裡頭大量存在 @ 符號,這有何意義?你能否應用在後續的程式開發呢?提示: 對照看 Doxygen 因為有多 doc 是由程式像是 Doxygen 生成的,透過在原始碼中加入@的註解,可以被這些自動生成 doc 的程式解析,並輸出doc。 ### tests/ 目錄底下的 unit test 的作用為何?就軟體工程來說的精神為何? 測試程式的 robust 程度,甚至測試 worst case, edge case,看要跑多久,對於軟體工程來說,測試非常重要,如果沒有測試你不知道你的程式在怎樣的情況下能夠 handle ,比如說,台灣股票市場曾經因為大力光股票價格太高,導致計算過程中 overflow 造成結果計算錯誤,這就是系統沒有經過單元測試的結果。 ### tests/ 目錄的 unit test 可如何持續精進和改善呢? 暫無