contributed by < Kevin-Shih
>
注意作業書寫規範:中英文間用一個半型空白字元區隔。留意細節。
:notes: jserv
q_new
q_free
q_insert_head
q_insert_tail
q_remove_head
q_remove_tail
q_size
q_delete_mid
q_delete_dup
q_swap
q_reverse
q_sort
qtest
提供新的命令 shuffle
qtest
提供新的命令 web
若 head
為 NULL
則直接返回 0 ,否則以 list_for_each
遍歷整個 queue 來計算 size (其中 len
變數作為計數器)。
不要只張貼程式碼,你應該解釋程式碼運作原理,並提及你認為後續如何改進。
:notes: jserv
"malloc return null pointer when allocate failed or allocate zero space."
因此當 q 為 NULL
時直接 return NULL
以避免後續 INIT_LIST_HEAD()
嘗試對其初始化
由於 q_insert_head
及 q_insert_tail
都需要為新的 element
及 element->value
分配各自的空間,因此將這部份獨立出來以 q_new_element
處理並將輸入的字串複製到 element->value
指向的空間。
參閱 CERN Common vulnerabilities guide for C programmers 中建議避免使用 strcpy 等涵式,故採用 CERN 提及的替代方案 strncpy 處理字串複製。
使用 list_for_each_safe()
實做,避免修改 node->next
時導致無法正常遍歷整個 queue 以及修改 node->prev
時無法使用。 (list_for_each_safe()
會將 node->next
先暫存在 safe
)
為避免 NULL queue operation 先判斷 l
是否為 NULL
,若不是才繼續以 list_for_each_entry_safe()
遍歷 queue 中所有 element 並以 q_release_element()
釋放所佔用的記憶體。
使用 list_entry
取得 queue 中首個 entry 的指標 (由於接下來會修改 head->next
故須先取得其指標),接者重新調整下個 node 的 prev
指向 head
及 head->next
指向下個 node ,來繞過被 remove 的 entry 。
當 sp
不是 NULL
時,從被 remove 的 entry 複製最多 bufsize - 1 個字元到 sp
。
一開始的想法很直覺的以 q_size
取得 queue 中有多少個 node ,再計算 mid 的位置後,以迴圈將 pos
指標移動到該 node 。 調整前後 node 的指標來繞過該 node ,最後以 list_entry 搭配 q_release_element 釋放該 entry 佔用的記憶體空間。
TODO
在以 q_size
取得 queue 中有多少個 node 時須以迴圈形式遍歷整個 queue ,接者在移動 pos
時又再次使用迴圈,考慮將兩者整合為使用一次迴圈來改善效能。
另外,詳閱「你所不知道的 C 語言: linked list 和非連續記憶體」!
slow
及 fast
分別指向相鄰的兩個 node ,並將兩者交換(next 及 prev 重新指向正確的 node),交換完後 slow
及 fast
會指向下兩個 node 準備交換,重複直到 fast
再次指向 head
或 head->next
。
下次不用說「待補」,而是明確說出你的規劃和準備。
:notes: jserv
slow
及 fast
兩個 pointer 用來指向相鄰的 node 並比較兩者是否 duplicate 直到首次出現 duplicate 的情形後 fast
會繼續向前,停在 duplicate 情形結束的 node ,而 slow
則停在 duplicate 情形開始前的 node ,接者依序釋放 slow
及 fast
間的 node 所佔用的記憶體空間,並將 slow
接上 fast
的 node 。
TODO
補上 q_delete_dup 的改善方法。
最初使用 iterative 方式實做的 q_sort ,在標注 devide
的部份先以迴圈方式將除了 head 以外的 entry 分割為獨立的 entry (prev
, next
都設為 null) 再存入 queue
指標陣列中,視為單一一個 sorted list。這也造成目前最大的問題, line 22 宣告的 list_head 陣列 queue
大小有限,無法排序較大的 queue。 (queue
用於存放分割後的單一個 entry ,以便後續 merge)
參考 kdnvt 的開發紀錄,q_sort 的部份提到使用每個 sorted list 的 tail->next 指向下個 sorted list ,預計以這樣思路改寫並維持 iterative 的方式。
實做上述作法後,發現雖然可以正確排序,但由於處理大佇列時仍然過慢 (很可能是在 merge_2_queue
修補 prev
時需重新遍歷,拖累速度),因此仍過不了 trace_14
。
最後採用最典型的 merge sort 解法,將其視為 singly linked list 做完 recursive 的 merge sort 後在一一走訪節點,修復 prev
以及 circular 的特性。這種作法的速度足以通過 trace_14
。
TODO
補上 q_sort 改善方法。
根據 lib/list_sort.c 中的註解可以發現在做 merge 時是將 list 視為 null terminate 的 singly linked list 的藉此省去了每次合併時修復 prev
的操作,而只在最後一次合併時才改為呼叫 merge_final 以修復 prev
並維持 circular 特性。而在 merge 的過程中 prev
則用來連排序好的 sub list 。
在讀對應的 commit massage 時 George Spelvin 提到 top-down 的方式雖然可以很好的分割 sublist 來達到 balance 的 merge 但由於需要知道 linked list 的 size 需要一一走訪每個節點,當 list 超過 L1 cache size 時更會造成大量的 cache miss。
lib/list_sort.c 採用的是 buttom-up 的方式,然而並非完全的遵照 depth-first order ,儘管 depth-first order 可以很好的善用 L1 cache ,通常只有最後幾次 merge 為超出 L1 的大小而產生 cache miss。 但當 n 比 2 的冪次稍大時(例如:1028),最後的 merge 將會不平衡。 George Spelvin 提到為了避免這樣的情形發生,合併時將會盡可能維持最差 2:1 的合併比例。
定義一個 function pionter list_cmp_func_t
稍後指向自定義的 compare function ,其中這個 compare function 應是 (pointer, pointer to struct list_head const, pointer to struct list_head const) 並 return int 的形式。
參考研讀 Linux 核心的 lib/list_sort.c 原始程式碼中關於自訂 compare function 的方式。
除定義 compare function 及補上 define likely 和 unlikely 外均與 lib/list_sort.c 中相同。
採用 K 值作為比較基準,其中 K 值定義如下:
自行定義的測試命令中,由於該 commit 提到當 n 比 2 的冪次稍大時,最後的 merge 會非常不平衡,因此決定測試 n 到 1.25n 間的平均 K 值來了解兩個實做間的差異。 (一方面也是節省測試所需時間)
寫完測試程式後忽然想起,我自己實做的版本是 Top-down 的方式,取得 list size 後從中間對半切再遞迴呼叫 merge_sort ,因此理論上會有較好的 K 值,但實際計時時應該會被取得 list size 時走訪個節點的耗時(及很有可能發生的 cache miss)拖累,相較 list_sort 慢上不少。
以下為實際測試結果:
果不其然,自己實做的版本 K 值稍大,約 0.03 左右,接者來測試實際執行時間。
使用 qtest 中的 time 命令來測試兩者在 50,000 和 100,000 及 500,000 隨機字串下排序的耗時。
50,000 隨機字串耗時:
100,000 隨機字串耗時:
500,000 隨機字串耗時:
比較成果:
Sort | 50,000 time (s) | 100,000 time (s) | 500,000 time (s) |
---|---|---|---|
自行實做 | 0.052 | 0.121 | 0.720 |
list_sort | 0.036 | 0.099 | 0.451 |
折線圖:
在比較實際執行時間時,top-down 作法的劣勢馬上顯現出來,3 種測試的佇列長度執行時間都比 list_sort 慢上不少。
TODO
研讀維持 2:1 的 code 究竟是如何達成。
在閱讀 Fisher–Yates shuffle 演算法後,發現交換兩個 node 位置可以藉由兩次 list_move_tail
來完成,先將 cur
指向第 j 個 node (其中 j 以隨機亂數得到),dest
指向要交換的 node 的下一個 node 。 接者將 dest->prev
透過 list_move_tail
移到 cur
之前,再以 list_move_tail
將 cur
移到 dest
之前的位置。
Shuffle 前:
假設第一次選中 1
:
如上圖所示當選中 1
時 cur
會指向該 node , dest
則指向 head
。
第一次 list_move_tail
後:
呼叫 list_move_tail(dest->prev, cur)
將 dest->prev
(即 node 3
) 移到 cur
的 prev
處,如上圖所示。
第二次 list_move_tail
後:
呼叫 list_move_tail(cur, dest)
將 cur
(即 node 1
) 移到 dest
的 prev
處,如上圖所示。 最後將 dest
改為指向 dest->prev
即完成一次交換。
最後將 dest
改為指向 dest->prev
:
cur
會在下次選中 node 時重新指向該 node 。
graphviz 參考資料:
https://stackoverflow.com/questions/70441786/draw-doubly-linked-list-using-graphviz
以及:
kdnvt 的開發紀錄
首先將 tiny.c
放入 lab0-c,並修改 makefile 來編譯它。
接著修改 tiny.c
,刪去其中部份用不到的 function ,並修改 main
。
讓 main
僅負責啟動 server 並將 socket 對應的 fd 寫入 listenfd。 另外也修改 parse_request
如下。
修改後的 parse_request
除了取得 request 內容外,也將 req->filename
處理為 cmdline 的格式(替換所有 \
為 space
),並印出 LOG_ACCESS 。
接著為了方便接入 qtest
,將 tiny.c
中用的到的 function 放到自行建立的 tiny_web.h
供 qtest
呼叫。
建立
tiny_web.h
參考:
laneser的開發紀錄
接著在 qtest
中加入 web
指令,並實做 do_web
function。
根據作業要求:
先將 socket 改為 non-blocking,防止程式停止接收使用者輸入。
由於作業要求透過 select()
讓 qtest
在 web
或 commandline
輸入命令時就會執行相對應的命令,因此接下來就以作業要求中 linenoiseEdit()
的範本來修改。
linenoiseEdit()
中關於 select()
及處理 web 命令的部份:
在處理接收到的 request 改採先前修改的 parse_request()
取得以處理過得 req.filename
並將其複製到 buf
中作為該次 command。此外,這邊將關閉 connfd
延後,方便 command 完成後將執行結果回傳給 web 端。
修改 report.c
中顯示執行結果的 report()
及 report_noreturn()
,當 connfd
未被重設為 0
時,將結果回傳給 web 端。
最後修改 console.c
中的 run_console()
,讓程式在執行完該次 command 後,若有 connfd
則關閉該 fd 避免 web 端持續等待回應。
server 端:
client 端:
TODO
解釋 select 系統呼叫在本程式的使用方式,並分析 console.c 的實作,說明其中運用 CS:APP RIO 套件 的原理和考量點。
論文中提到以 Welch's t-test 來檢定兩個不同 class 的測試資料的實際執行時間(如: cycle 數)是否來自(平均執行時間)不同的母體。其中一個 class 是固定的 input ,另一個 class 則是隨機的 input,在 lab0-c
中即對長度等於 input 長度的佇列進行操作(ih, it, rh, rt)。
Welch's t-test 是一種對於 student's t test 的修正,對兩樣本的變異數不同且(或)樣本數不同的情形下更為可靠。 (由於執行時間
屬於變異數未知的資料故採用 Welch's t-test)
由於這篇論文所提出的方式是以實驗收集不同輸入條件下的執行時間來進行統計分析(檢定),因此收集到的資料好壞、樣本數大小非常重要,當樣本數不足時可能無法辨別兩個 class input 的執行時間是否屬於不同母體(即非常數時間)。此外,這樣的統計分析所得到的結果也僅限於當前收集到的 筆資料,無法進一步推斷 時是否會是相同的結果(哪怕 再怎麼趨近無窮大)。
其中關於資料的好壞,在 lab0-c
的實做中與論文中同樣為避免準備測試資料等待測的 function
以外的因素干擾,準備測試條件及資料的時間並未被記入。下方以測試 ih
為例:
TODO
drop_size
作用?
在 lab0-c
的實做中存在一些疑點,相較於論文中的實測往往達到幾十萬、百萬筆資料,lab0-c
中 #define enough_measure 10000
定義足夠的測資為 1 萬筆資料是否足夠,值得思考。
使用 valgrind 檢測直接由 cmdline 輸入的情形
該情形下並未產生錯誤,經測試以 ctrl + c
中止 qtest
也無錯誤。
ctrl + c
中止使用 valgrind 測試當 qtest
以 -f 的方式讀入 trace-xx-ops.cmd
執行的情形
該情形下並未產生錯誤,查看 git log 時發現是 commit #d39497 中修復了在讀取檔案作為輸入時不必要的初始化了 linenoise 相關功能。
run_console():
未修正前由於 run_console()
會進入下方 cmd_select() 的部份,並不會執行到 linenoiseFree()
因此導致 linenoise 初始化讀入的 history 未被釋放。
另外,在該 commit 也修正了當命令執行失敗時應先執行 finish_cmd()
釋放佇列所佔用的記憶體。(原先若 ok == false
就不會再執行 finish_cmd()
)
使用 valgrind 測試當 qtest
以 <
導入 trace-xx-ops.cmd
中命令的情形
由於不是以 -f 方式輸入檔名,而是 <
重新導向的方式讀入命令,因此前述關於讀檔時 linenoise
不做初始化的修改便失去效用了,參考 freshLiver 的開發紀錄以 <
重新導向會讓 linenoise 進入 linenoiseNoTTY 函式而其中並未與 enableRawMode()
中一樣指定在程式離開時要執行的函式。
修正後重新測試,valgrind 不再回報錯誤: