contributed by < chiangkd >
Got it! 謝謝老師
Request followed 2023 年 Linux 核心設計/實作課程作業 —— lab0
queue.[ch]
以完成 make test
自動評分系統的所有項目
qtest
實作的記憶體錯誤,搭配 Massif 視覺化qtest
提供的 web
命令可以搭配上述佇列使用shuffle
, 參考 Fisher–Yates shuffleqtest
中執行 option entropy 1
並搭配 ih RAND 10
,觀察亂數字串分佈queue.c
list_head
為 doubly-linked list 的 node 或 head
element_t
為 linked list 的 element
element_t
這個結構體,除了要用來找尋 list
整體的原因,透過嵌入 list_head
給除了
Head
可以搭配 container_or
巨集也可得知
queue_context_t
為 chain of queues
在多個 queue 之間的操作,如 do_queue
是透過 chain
來連接起來的,也因此在 q_merge
function 中給定的參數為 list_head *head
,但查看 do_merge
之後才會看到這裡傳入的是 queue_chain_t
的 chain
,也是連接各個 queue 的 linked list。
做個驗證,在 q_merge
中來測試並呼叫 merge
command,函式中暫時先撰寫透過傳入的 chain
找其所屬 element_t
的 value
element_t
的 value
與其複製程式碼,你應揣摩背後的設計考量,特別是你的推理和驗證。
新增示意圖及個人認為設計考量(待補充)
題外話,在化 graphviz 的時候想要虛線功能,但不知道關鍵字是什麼 (加粗為 bold
,直覺認為是 dot
),問了一下 chatGPT 之後它告訴我是 dotted
改為更精簡的敘述:
修正程式碼註解用語。
Fixed. Thanks!
能否避免呼叫 memcpy
並精簡程式碼?
改為利用
strdup
發現在 harness.c
中已有定義 strdup
(test_strdup
),此函式回傳輸入 string s
的指標,並回傳一個具有同樣內容的指標,即複製字串。
透過此函式可以直接取代掉原實作方法中的計算長度、分配空間、記憶體複製操作
和 q_insert_head
思路相同, 僅修改 head
和 tail
的差別
既然 q_insert_tail
和 q_insert_head
存在相似的流程,能否用 q_insert_head
來實作 q_insert_tail
呢?
新增相關敘述
q_insert_head
以及 q_insert_tail
差異僅在新增節點於給定 head
的後一個位置 (next
所指方向),以及前一個位置 (prev
所指方向),故可以透過 q_insert_head
來實作 q_insert_tail
翻譯成白話文的意思就是,在 head
的前面插入節點等效於在 head
的前一個節點的後面插入節點。
container_of
這個巨集透過給定成員 (member) 的記憶體地址、結構體的型態,以及成員的名稱, 傳回該結構體的起始地址上述雖然可以成功 remove head, 但是產生錯誤訊息
查看 qtest.c
中 do_remove
的實作
implementation 應翻譯為「實作」,注意「作」和「做」的落差」。
參見 資訊科技詞彙翻譯 的 "implement" 項目。
已修正,謝謝老師
removes[0] = '\0'
會報此錯誤, 且 removes
在 qtest.c
已經 malloc
如果在 q_remove
中再次 malloc
一次的話 sp 的地址會被覆蓋掉, 即原先 removes
的地址其實並沒有存東西, 所以這裡的 sp
不該 malloc
在佇列為空時繼續使用命令 rh
會造成 segmentation fault, 當佇列為空時僅有 Head
一個點, 沒辦法通過 container_of
找到對應節點
加入 list_empty
來判斷該鏈結串列是不是 empty 以及外部 removes
是否有 malloc
成功
這裡的 container_of
可以用 list_first_entry
巨集代替, 增加程式可讀性 (defined in list.h
)
list_last_entry
取代 container_of
和 q_remove_head
思路相同, 僅修改 head
和 tail
的差別
走訪整個鏈結串列以計算長度
TODO: 善用既有的巨集,撰寫出更精簡的程式碼
用
list_for_each
取代現有 for loop
其中 list_for_ecah
定義為
本例為 circular doubly-linked list,你應該實作更少記憶體操作的程式碼。
原先版本使用快慢指標,快指標會走訪整個 list 一圈,慢指標會走一半的 list 找到中間點,總共會走大概 1.5 倍長度的 list,參考 freshLiver 過往作業使用到兩個指標前後從佇列頭尾迭代,透過交會點/即將交會點來判斷佇列中間點
在這邊使用 indirect pointer 看起來沒有特別的優勢,反而在過程多了 *indir
這一個記憶體操作
對照編譯器輸出的組合語言和 perf
來確認你的觀點,否則這個「看來」流於武斷。注意編譯器最佳化的影響
初步分析以補充,待補如果設定編譯器為
-O0
後的組合語言差異
且如此運算過後 n
會正好指向要移除之節點,不需要在額外設定指向要刪除的節點進行操作,
用 objdump -d queue.o
查看編譯器輸出的組合語言
結果上述的兩個版本編譯器編出來的組合語言竟然是一樣的,查看 makefile
發現編譯器優化等級設定在 -O1
,如果禁用優化改成 -O0
編譯出來的組合語言才會有差
定義一個測試案例 trace-demid.cmd
輸入命令
perf stat
可以測量單個程序運行時間內的性能係數
--repeat 5
重複 5 次-e
後面接著 PMU 的事件,可以用 perf list
查看所有事件結果如下
利用快慢指標 (Floyd's tortoise and hare) 找中間點, 因為在本次作業中已經確保快慢指標 (或龜兔指標) 已經都在環中, 所以設定快指標移動速度為慢指標兩倍, 當快指標繞完一圈時慢指標指向中間點(有 Head
節點則差一個節點)
(*indir)->next
直接 release 掉的話會造成 (*indir)->next
指向 NULL
這不是我們想要的, 這裡我先用 list_del((*indir)->next)
將 (*indir)
和 (*indir)->next->next
linked 起來,在將原先的 (*indir)->next
release 掉Iteration Process
以 2095. Delete the Middle Node of a Linked List 實際演算一次
(*indir)->next
節點首先思考使用 list_for_each_entry_safe
實作,根據描述
list_for_each_entry_safe - Iterate over list entries and allow deletes
會走訪整個 list 的 entries (typeof(entry)
) 並允許刪除當前的節點
執行 list_for_each_entry_safe
可以走訪整個 list 但是需要注意在走訪完時 n
會指向 head
,且 head
是無法透過 container_of
找到節點的,因為它沒有嵌入到結構體中,這時如果對 n
取 contain_of
會拿到 NULL
如果再做取值 n->value
會報錯 成為無效的記憶體操作。
故在第一個 if
條件使用 c->list.next != head
來判斷是否來到最後的節點 (雖然 list_for_each_entry_safe
本身就有做),避免在 strcmp(c->value, n->value) == 0
時 n->value
對 NULL
指標取值。
TODO: 減少記憶體操作的數量
交換佇列中鄰近的節點,將兩個鄰近的節點中的第一個節點移動至以令一個當作 head
節點的開頭 (也就是說會放在另一個節點的 next
),移動節點可分為兩步驟(刪除以及插入),對應到 list_move
中定義的兩步驟 list_del
以及 list_add
一開始原本是考慮到沒有刪除節點的問題所以使用 list_for_each
來走訪 list ,但是在 reverse 的過程中的 node
會把 next
和 prev
做交換,會讓原本定義的巨集出現錯誤,導致無法順利走訪。
改用 list_for_each_safe
在迭代過程中 safe
指標會始終維持在 node
的下一個節點(沒有 reverse 的),使用 node = safe
以及 safe = node->next
確保迭代過程 node
不會往回走。
此函數 函式需要將 list 中的 k 個節點進行 reverse,直覺來說會利用到上方已經實作完成的 q_reverse
,為此在使用時符合 q_reverse
參數需求,傳入該 list 的 head
,故使用 list_cut_position
對特定長度的 list 進行分割,並在分割下來的 list 頭部新增 iter
作為該 list 的 head
供 q_reverse
使用
注意用語,參見 資訊科技詞彙翻譯。上述 "reverse" 應有對應的漢語描述
實作 merge sort,先將環狀結構斷開後,傳入 head->next
進去 merge_recur
merge_two_list
TODO: 撰寫更精簡的程式碼,縮減分支
merge_recur
避免使用含糊的「中點」,應有對應的定義及說明。
q_sort
head->next
進行 merge sort 可以保留原本的 head
,在排序過後在進行 prev
以及 circular
的修補此函數會將右邊存在有嚴格大於的節點的節點給移除 (remove)
如果順著 list 方向進行走訪 (以 next
指標迭代),無法確定迭代的過程中會不會遇到嚴格大於的節點(如果遇到則走放過的節點都要移除),故這邊我以反方向進行迭代(以 prev
指標迭代),找尋下一個節點值只要和當前節點做比較,若下一個節點小於當前節點的話則移除。
執行測試案例的時候發現 trace-06
過不了,發現是 descend
後被消除的 block 沒有被清掉,一開始看到函式敘述用 remove
時以為不用刪除
Remove every node which has a node with a strictly greater value anywhere to the right side of it
但它也沒有存到像 q_remove
系列的 sp
中,由 q_test
負責刪除,故修正在函式內進行記憶體釋放
先看一下 do_merge
要我們做什麼
且在 do_merge
中會負責把被 merge 掉的 queue 空間 free
掉,接著再檢查是否真的有按照 ascending order 來排序
實作過程中利用在 merge sort 已經寫好的 merge_two_list
,不過須注意的是這邊我的 merge_two_list
傳入值為兩個沒有 head
的 list,也就是每一個節點其對應到的 element 都有數值,且不是環狀 linked list ,實做過程將兩個已經 sorted 好的 list 傳入 function 並回傳將兩個 list merged 完成的 list 的第一個節點地址,此時可以把 merged 好的 list 交給主要的 context (迭代中的) 的 list head (其 element 沒有值的那個)
完成之後會有一個 merged 好的 list,接著將環狀的結構復原 (prev
被各個佇列的 merge 打亂了,僅有 next
為正常順序)
最後把處理好的 list 透過 list_splice
交給主要 chain head
所屬的 element
此節根據〈Dude, is my code constant time?〉修正測試案例 trace-17-complexity
檢測程式碼不會常數執行時間的問題,在 trace-17-complexity
測試案例中會將 option simulation
打開,並執行 insert head (ih
)、insert tail (it
)、remove head (rh
)、remove tail (rt
) 指令,而在 qtest.c
中相對應的 do_it
、do_ih
、do_remove
都有對應 simutlation
開啟狀態下的行為。
下述程式碼已在 #127 更新
首先查看 do_ih
在 simulation
下,對應到三種結果
是否為 constant time 由 is_insert_head_const()
函式回傳判斷,
is_##x##_const
系列函式由前置處理器 concatenation 處理,其中 insert_head
, insert_tail
, remove_head
, remove_tail
也都是由前置處理器處理。
這技巧稱為 X macro
Got it! Thanks
constant.c
中的 measure
函式負責判斷 ih
、it
、rh
、rt
是否有正常運作 (透過 size 有沒有隨著 insert 以及 remove 正常的變動)
fixture.c
中的 report
函式則負責測量是否為 constant time
此處流程可以參照 preparaz/dudect,中的 Checking your code for constant time 節
dudect
is distributed as a single-file library for easy building. Steps:
- Copy
dudect.h
to your include directories- Add #include "
dudect.h
" from your source files.- See this minimal example for a fully contained example. You'll need to write the following functions:
do_one_computation()
,prepare_inputs()
and- call
dudect_main()
from your main function
實作放在 ttest.c
相關函式,按照公式計算 t
值
其中 t
值越小代表兩組分佈越接近,注意這裡的 指的是 sample mean
首先在進入定義全域變數 t_constext *t
,結構體定義為
在每一次測試時 (預設測試 10 次 (TEST_TRIES=10
)),首先呼叫 init_once()
,在 init_once()
中會將 t
的各個元素初始化。
接著根據不同的 mode
(不同的 case 有不同的 mode number) ,進入 doit
,在 prepare_input
之後進入 measure
函式, measure
函式根據 q_size
判斷 ih
, it
, rh
, rt
等操作有沒有順利進行,也計算函式執行時間,以 insert_head
為例:
函式若正常執行回傳 true
給 ret
,且更新了 before_ticks
以及 after_ticks
交給 differentiate
進行計算 exec_time[i] = after_ticks[i] - before_ticks[i]
,其中 i
為每次 test
進行的 measurement 次數,預設為 150 次。
接著將計算出的 exec_time
及 classes
送入 updata_statistics
,函式內透過 t_push
進行 t-test ,t_push
需要的參數包含
t_context
本體 t
difference
也就是對應的 exec_times[i]
classes[i]
在 dudect.h 原始碼中有提到可以參考《The Art of Computer Programming, Volume 2 : Seminumerical Algorithms》 (待研究),看起來是透過取新的 execution time 與當前平均值的差為變化量 (delta
) 而新的平均值就會是舊平均值加上變化量的平均值 (delta/n
),可以避免每次都將所有數值相加取平均的過程,而 m2
為變異數尚未平均的值,在後續 t_compute
會使用到,每次的 m2
更新為舊 m2
加上 delta
乘上這次的 execution time 與更新過後的平均值的差。
在 ttest.c
中
之後進入 report
函式透過 t_compute
及取絕對值的 fabs
計算 max_t
, max_t
數值就會根據 t_threshold_xx
的值來判斷程式是否 constant time,這裡程式的實作如同上述給定的公式
再回傳 t
值之後會在透過 fabs
取絕對值。
當前 lab0-c
並未實作 percentile 功能,也就是沒有將測量誤差給 crop 掉,將 dudect.h 中的 percentile 引入後即可順利通過 trace-17-complexity
dudect.h 中的結構體定義和 lab0-c
有一些不一樣,原專案中使用 dudect_state_t
將 ttest_ctx_t
(等效 lab0-c
中的 t_context_t
) 及執行時間、輸入資料、等必要資訊定義在一起,但在 lab0-c
都是拆開的,在這邊引入 percentile
至 t_context_t
中
詳細改動在 commit 中。
將 list_sort.c 以及 list_sort.h 引入專案,並新增 makefile
的 dependency 以及把無關的 include header 移除
list_sort.h
編譯後發現 likely
以及 unlikely
沒有定義,參考 linux/compiler.h,其中的 __builtin_expect()
是 gcc 的 built-in function,用來將 branch 的相關資訊提供給 compiler,讓其對程式碼改變分支的順序
在 list_sort
的 prototype 中需要三個參數,priv
, head
,以及 cmp
head
為我們要輸入的 queue 的 head
cmp
為 compare function,作為函式指標傳入,根據註解The comparison function @cmp must return > 0 if @a should sort after
@b (“@a > @b” if you want an ascending sort), and <= 0 if @a should sort before @b or their original order should be preserved. It is always called with the element that came first in the input in @a, and list_sort is a stable sort, so it is not necessary to distinguish the @a < @b and @a == @b cases.
cmp
return > 0 (a 排在 b 的後面)cmp
return <=0 (a 排在 b 的前面或保留原本排列)list_sort
是一個 stable sort,故不用區分小於 0 或等於 0不確定這邊的 priv
是要幹麻的,不過根據宣告 priv
可以為 NULL
參考 do_sort
新增 do_lsort
代表 linux 核心原始程式碼的 list_sort
並定義 cmp
提供新命令 lsort
在 qtest.c
的 console_init()
中新增命令
結果呈現
git commit
的時候會被擋下來
考慮到不要修正 list_sort.c
的原始碼,用 cppcheck-suppress
跨過去
使用 perf
進行效能比較,參考 Linux 效能分析工具: Perf
定義測資
<sort>
可以為原先 qtest.c
中撰寫的 do_sort
或是引入的 do_lsort
,透過 time
命令獲取 sort
執行的時間,設定 --repeat 5
重複五次,並透過 perf
拿到 instructions
以及 cycles
的數據自行撰寫的 q_sort
q_sort | time |
---|---|
1 | 0.487 |
2 | 0.477 |
3 | 0.488 |
4 | 0.486 |
5 | 0.529 |
Instructions | Cycles |
---|---|
20,9111,6747 | 36,2842,8338 |
list_sort
q_sort | time |
---|---|
1 | 0.411 |
2 | 0.417 |
3 | 0.423 |
4 | 0.501 |
5 | 0.460 |
Instructions | Cycles |
---|---|
21,4157,1467 | 34,1147,9158 |
雖然每次執行這個測試案例時,Instructions 和 Cycles 的數量會些微變動,但整理來看
list_sort
指令較多,Cycles 較少選取一個隨機數,將該位置元素取出後放到尾端,舉例來說
隨機範圍 | 隨機數 | 原始數據 | 結果 |
---|---|---|---|
12345 | |||
1~5 | 4 | 1235 | 4 |
1~4 | 3 | 125 | 34 |
1~3 | 1 | 25 | 134 |
1~2 | 1 | 5 | 2134 |
在 qtest.c
中的 console_init()
新增 shuffle
指令
查看其巨集定義
此巨集會將 cmd
命令的行為去透過巨集展開 do_##cmd
去呼叫對應 handler ,如其他命令 new
會去呼叫 do_new
函式,故在此定義 do_shuffle
函式來 handle shuffle
命令
參考其他函式 handler 的寫法,有些函式內有定義 bool ok
,有些則無,觀察到 ok
都用在佇列長度有更改或者需要判斷佇列是否為空的情形,所以 reverse
,swap
中不需要 ok
來處理佇列長度改變,且對應的 do_reverse
以及 do_swap
中都有相應的 if(!head)
來處理空佇列的問題,綜上所述 do_shuffle
不需要有 bool ok
定義。
q_shuffle
宣告加在 queue.h
所以就放在 do_shuffle
的上面首先嘗試用 valgrind
跑跑看 trace-01-ops.cmd
測資
依據網站及作業講解敘述,Valgrind 有 5 種 memory leak
這裡發生的 still reachable 在 do_new
及 test_malloc
中,但是如果直接執行,qtest
且逐行輸入命令不會有這個問題。
沒什麼頭緒,參考 yanjiew1,上述問題已在 #121, #122 被修正
TODO: 搞懂別人做了什麼
選用 trace-17-complexity.cmd
先透過 Valgrind 產生 massif 檔案
再使用 massif-visualizer
查看
trace-eg.cmd
進行分析alanjiang85: 要注意的是,挑選適當的測試資料時要避免效能類型的測試,因為以 Valgrind 執行測試程式會比較慢,容易導致出現超過時間限制的狀況。
參考資料不是讓你發表致謝詞的地方
Got it, Thanks!