contributed by < millaker
>
queue.h
和 list.h
了解 queue node 是如何定義注意程式碼排版風格,以 4 個空白字元進行縮排。大括號 ({
) 的位置要留意
佇列的每個 element 用 list.h
內提供的 struct list head
來連接, struct list head
內含兩個指標分別指向前一個和下一個節點, 如果是空的 list,則 next
和 prev
皆指向目前的 list_head
。
queue.c
程式碼實作q_new
:Linux Kernel API 有提供 INIT_LIST_HEAD
,較好閱讀。
因為 list_head
包含在佇列的 element_t
內, 利用 list_head
走訪每個 element_t
時, 必須得到包含此 list_head
的 element_t
的地址,所以要用到 你所不知道的 C 語言: linked list 和非連續記憶體 內講到的 container_of
巨集。
第一次嘗試的作法如下:
在執行 make check
時,在程式結尾會出現以下錯誤:
從 qtest.c
追溯問題,得知 quit
命令會再呼叫一次 q_free
, 如果在執行 quit
前就已經釋放所有佇列節點,傳進去的 list_head
會是 NULL
,於是第一行 &l->next
便會產生 deferencing a NULL pointer 的問題。經過修改,新增一個檢查和把 container_of
換成更容易看懂的 list_entry
。
q_insert_head
和 q_insert_tail
這兩個函式的實作方法很相近,所以放在一起,依序檢查 head
、temp
、 val
在傳入或 malloc
過後是否為 NULL
,就不用再多使用free()
來釋放前面已配置的記憶體。malloc
都正確執行後,雖然上課中有講到 strcpy() 和一些常用函式的危險性,但在這邊,分配給val
的記憶體大小都是確定的,不會產生字串大小超過 buffer size
的疑慮。最後再使用Linux Kernel API 所提供的 list_add()
和 list_add_tail
將新的 list_head
插入。
詳細閱讀 Linux Kernel API 提供的 list_head
操作巨集及函式,發現有許多好用的功能,
以下實做都會盡量使用,前面可更改的部份後續處理。
這裡用到幾個 list.h
提供的API,list_last_entry
, list_first_entry
, list_del_init
。
可以在 list.h
內觀察到,其實list_last_entry
和 list_first_entry
就是重新包裝過後的 container_of
。
測試將 NULL
傳入 q_remove_head
和 q_remove_tail
時會發生錯誤,發現在開頭檢查少一條件,加上 list_empty(head)
更新: 沒有考慮到移除的字串大小大於 buffer 的情況,需要在結尾補上 '\0'。
和 lab0-牛刀小試 一樣
Linux Kernel API 中是有提供 list_swap
一函式,但是本次作業中 list.h
並沒有此函式。
因為 list_swap()
有在不只一個地方使用,於是寫成 helper function並取代掉原來的部份,讓程式碼更精簡。
使用兩個 list_head
指標 front, back
分別指向第一個和最後一個 list_head
,每個步驟中分別將 front
往後指一單位 back
往前指一單位。
發現有其他地方也會需要找到中間節點,所以把它寫成函式 q_find_mid
不過在寫mergesort時,因為將佇列轉成 NULL-teriminated singly-linked list,函式裡的 prev
都會失效,於是參考上課講義內使用兩快慢指標來找到中間節點。
使用兩個指標 curr
和 it
, curr
指向當前要比較的值,由 it
往下將值相同的節點都刪除並加入由 removed
為 head 的一條新佇列,最後在呼叫前面實作的 q_free
將所有重複的節點與 removed
一同釋放。
將每個節點依序放到佇列的尾巴
最初的想法是將鏈結串列拆成兩個有 head
的串列,再進行 mergesort,但是在 q_sort 內 malloc
是不允許的。在無法給拆解的兩條串列 head
的情況下,要操作 doubly-linked list 實在非常困難,想了很久實在想不到方法,所以先閱讀 linux/list_sort.c
才知道先將 doubly-linked list
轉成 singly-linked list
。
其中 merge_sort_list
是參考課程講義內 你所部知道的 C 語言:linked list 和非連續記憶體 - Merge Sort 實作 快慢指標找中間節點的方法,這裡無法使用我自己寫的 q_find_mid
是因為已經不是 doubly-circular-linked list,無法直接存取 head->prev
。merge
函式則是使用同一篇講義內 Merge two sorted list 使用 indirect pointer的方法。關於 linux/list_sort.c
內使用的方法目前還在研讀。
以上就是原有功能的實作,在執行 make test
時, trace 17 time complexity test 無法在本機執行通過,但在 github action 上卻過了,觀察其他同學 github action 的動態,相近寫法的程式碼有些通過有些沒通過。
linux/list_sort.c 檔案內有這段註解在解釋使用了什麼特殊的改變,讓 merge sort 能夠減少呼叫 cmp()
的次數,也就是比較節點大小的次數,以下挑選出方法解釋的部份。
這裡使用一個 int count
來追蹤有多少已排序好的串列在 pending lists 中,如果有兩條長度為 的串列在 pending lists,後面有 條串列以上,則會 merge 這兩條得到長度為 的已排序串列。可以看上面註解中,把 count 以 binary 的形式來看, bit 不為零, bit 為零,也就是 state 2, 4,就會 merge ,並得到一條長度 的已排序串列。
程式碼實作的部份:
先將佇列改成 singly-linked list
,指標 prev 另作它用。
這段程式碼分三個部份,
count
找到需要被 merge 的串列以上迴圈重複執行直到 list 內全部的節點都輸入至 pending 。迴圈執行完後,剩下就是把尚未排序的 pending list 全部排序完成,呼叫 merge_final()
。
merge_final
會也會一併把 list 恢復成 doubly-circular-linked list
。
以後不要花太多時間 Google 搜尋,回到第一手材料。
linux/list_sort.c 內的註解沒有提到太多為什麼可以加速,在 google 上也找不到類似的實作方法,最後是去翻這個檔案的 commit history,發現原先 list_sort 並不是這樣實作的,在三年前才更新成現有版本,其中 commit message 寫的非常清楚:
linux/list_sort.c
commit b5c56e0
:盡可能的將 list 的前端排序好,只要有兩個相同的長度 的 pending list ,就合併他>們,這樣只要 的大小能放進 L1 cache, 就不會有 cache miss 的問題,只有最後
幾次執行的 merge 因為 list 總長度而強迫出現 cache miss。
在 merge()
和 merge_final()
內看到兩個疑似函式,likely()
和 unlikely()
,經查詢後得知並不是函式,而是 linux 定義的巨集,定義如下:
其中 __builtin_expect()
是 GNU 的內建函式 (builtins), 讓編譯器知道說會預期得到什麼值,likely()
和 unlikely()
就是讓編譯器知道這邊組合語言要怎麼編排才比較不會讓分支預測失敗,減少 pipeline flush 的次數。 參考
應該先查閱 gcc 手冊,而非看 stackoverflow 的討論,畢竟你現在不具備完整的判斷能力。
jservImage Not Showing Possible ReasonsLearn More →
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
了解,我會改掉這習慣。
為了將list_sort.c 引入我自己的 qtest
中,把一些 Linux kernel 才有的東西移除,像是
likely()
, unlikely()
, priv
,並自己寫一個 cmp()
比較函式。再用 ADD_COMMAND
的方法在 qtest
直接新增一個新的命令 lsort
。
使用 qtest
內提供的 time
來紀錄時間,測試用 it RAND 1000000
,先排序好, reverse 當作測試資料,所以兩個方法都會是排序一樣的資料。
以下有省略一些不需要的資訊
可以看出 lsort
比我自己寫的快約 37% 。
PERF:
使用 Wikipedia - Fisher–Yates shuffle 內 modern algorithm
,每次隨機選取小於 size - i 的節點, 其中 i 為執行次數,和最後一個節點交換
這裡使用的 list_swap
是參考 Linux Kernel API 實作的
在 qtest.c
init_console()
內新增
ADD_COMMAND(shuffle," | Shuffle the queue in random order");
便完成 q_shuffle
的功能擴充