contributed by < Lccgth
>
修正 GitHub 超連結
lintin528
在 list_sort
、 timsort
、 q_sort
的比較中,除了執行時間上的不同可以嘗試使用 perf 來分析 cache
的使用狀況,觀察各個演算法的特性。
file 的翻譯是「檔案」,而非「文件」(document)
好的,已修正!
我注意到 queue.c
文件 檔案原先未包含 list.h
標頭檔,而 struct list_head 的宣告位於 list.h
中。為了解決這一問題,我在 queue.c
檔案開頭添加包含 list.h
的指令 敘述 (statement)。
q_new()
commit 3b96677
建立一個空的佇列,並且如果記憶體配置失敗就返回 NULL。
先配置記憶體空間給head
,並檢查是否成功配置,接著使用函式 INIT_LIST_HEAD()
將 head->next
和 head->prev
都指向 head
。
將「目標」和「實作」手法置於本段落前方。
好的,已修正!
q_free()
釋放所有佇列使用到的記憶體空間。
先檢查佇列(head
)是否存在,接著逐一走訪佇列中的節點,紀錄每個節點的 entry,再從佇列中移除此節點、釋放entry
。
q_insert_head()
commit f4608c6
在佇列的開頭處插入一個節點。
先檢查佇列(head
)是否存在,和輸入的s
字串是否有效。然後為新節點配置記憶體空間,並驗證配置是否成功。接著為字串配置記憶體並進行複製。如果配置失敗,則釋放已配置的節點記憶體。最後將新節點插入到佇列的開頭處。
q_insert_tail()
commit 7efac73
在佇列的尾部插入一個節點。
與q_insert_head()
大致相同,只須將list_add()
替換成list_add_tail()
。
q_remove_head()
commit 6229a62
移除佇列開頭處的節點。
先檢查佇列(head
)是否存在,和是否為空,接著將字串複製到sp
中,最後將節點移除
q_remove_tail()
commit cc82605
移除佇列尾部的節點。
與 q_insert_head()
大致相同,只須將list_first_entry()
替換成 list_last_entry()
。
q_size()
commit 88d6c73
計算佇列的大小。
先處理head
為空的情況,接著逐一走訪每個節點,由此計算佇列的大小。
q_delete_mid()
commit 39e784f
刪除佇列裡中間的節點。
先檢查佇列(head
)是否存在,和是否為空,接著使用快慢指標找到佇列中間的節點,最後將其刪除
HackMD 不是讓你張貼完整程式碼的地方,GitHub 才是!因此你在開發紀錄只該列出關鍵程式碼 (善用 diff 標示),可附上對應 GitHub commit 的超連結,列出程式碼是為了「檢討」和「便於他人參與討論」,不是用來「假裝自己有付出」
了解,已進行修改!
q_delete_dup()
commit b215517
刪除佇列中有重複字串的節點。
逐一走訪每個節點,若目前節點和下一個節點的字串相同,則刪除目前節點,如果不相同的話,利用 dup
標籤檢查此節點的字串有沒有出現過,是否要進行刪除。
TODO: 撰寫出更精簡的程式碼。
q_swap()
將每兩個相鄰節點交換。
先檢查佇列(head
)是否存在,和是否為空,接著逐一走訪每兩個節點,每進行一組交換就要調整六個指標,分別是 cur->prev
的 next
、 cur
與 cur->next
的 prev
和 next
、cur->next->next
的 prev
。
使用 list_move()
將 cur
移到 cur->next
後方,一樣可達成目標,且程式碼更加精簡。
q_reverse()
反轉佇列中的節點。
先檢查佇列(head
)是否存在,和是否為空,接著逐一走訪每個節點,將 prev
和 next
交換。
只需要逐一走訪每個節點,並使用 list_move()
依序將其插入到佇列的開頭處即可。
q_reverseK()
commit c954812
將佇列中的節點以 k
個為一組作反轉。
先檢查佇列(head
)是否存在、是否為空、size
是否大於 k
,接著依照 k
個為一組走訪,將其中的每個節點作反轉。
善用 List API,撰寫出更精簡的程式碼
了解,我會依序檢查可以使用哪些 List API。
q_sort()
commit ae7aeba
將佇列中的節點升序/降序排序。
若目前佇列的節點數大於二,則使用快慢指標找到目前佇列的中點,並依照終點將其切為兩條佇列,直到所有佇列的節點數都只剩下一個,再開始依序將兩兩一組佇列進行合併。
q_ascend()
刪除 移走佇列中滿足條件的節點: 右方存在嚴格小於此節點的節點。
先檢查佇列(head
)是否存在,和是否為空,接著反向逐一走訪每個節點,同時紀錄目前所觀察到的最小值,若目前走訪的節點大於最小值,就刪除移走目前節點,反之更新最小值,並紀錄佇列中剩下的節點數。
可以不用建立一個字元陣列紀錄目前的最小值,直接使用指標獲取目前觀察到的最小值。
q_descend()
刪除 移走佇列中滿足條件的節點: 右方存在嚴格大於此節點的節點。
和 q_ascend()
大致相同,只須將紀錄最小值改成紀錄最大值。
q_merge()
commit abee78b
將所有佇列合併成一個升序/降序的佇列。
利用 list_splice_init()
將所有佇列先合併成一個佇列,並且同時紀錄目前佇列的大小,再使用 q_sort()
對其進行排序,最後回傳佇列的大小。
你如何確保目前的測試已涵蓋排序演算法的最差狀況?
此篇論文介紹了一個量測程式碼的方法 dudect,用以量測其是否能在常數時間 O(1) 內執行完畢。
對執行時間進行洩漏檢測,量測兩種不同輸入類別各自的執行時間,接著統計兩者在時間分佈上是否有差異。
避免非必要的項目縮排 (即 *
和 -
),以清晰、明確,且流暢的漢語書寫。
採用 fix-vs-random 的方法,將第一組輸入設定為固定值,而第二組輸入則會根據每次的量測隨機設定。
在進行統計分析之前對於每次的量測結果進行後處理,大部分的量測值會集中在較小的執行時間,而有一小部份量測值會出現特別高的執行時間,導致時間分佈出現正偏態,故須要對結果進行取捨。
使用 Welch 的 t 檢驗測試兩組數據的時間分佈是否相等,若結果顯示不相等,則意味著存在時間洩漏。
是一種機率分佈,用於依照小樣本來估計母體為常態分佈且標準差未知的期望值,由 v 值(自由度)控制其分佈的形狀,當 v 值越低,則出現極端值的概率較高,而 v 值越高,其分佈就越接近常態分佈。
我觀察到 oreparaz/dudect 程式碼中的 prepare_percentiles()
函式會根據不同百分位數計算出對應的 percentile
,而它們會在 update_statistics()
函式中被用於進行比較,若量測的執行時間小於對應的 percentile
,則將其用於統計分析中,大於就排除這些執行時間特別長的量測結果。
目前的 lab0-c 並未進行以上的檢查,會導致將一些執行時間異常的結果也納入統計中,導致程式碼的常數時間檢測會無法通過,當加入上述的檢查後,即可排除異常的結果。
列出論文和程式碼實作的出入之處,並予以討論。
論文與程式碼的主要差異有三,分別為:
cmp()
: 比較兩個 64 位整數的大小,用來當作執行 qsort()
時的比較依據。
percentile()
: 從一組測量值中找到特定百分位數的值。
prepare_percentiles()
: 根據不同的百分位數設定不同的閥值,並透過這些閥值來篩選測量數據,可由此排除執行時間異常的數據。
qtest
新增命令 shuffle
透過 Fisher-Yates shuffle 演算法對佇列進行洗牌。
走訪串列得知串列大小並設定 last
為最後一個節點。
從首節點到 last
節點中隨機取一個節點與 last
節點交換。
將 last
往前移一個節點。
重複步驟 2、3 直到 last
為首節點。
以 Graphviz 重新製圖,並嵌入到 HackMD 筆記中。
一開始先將 last
設為最後一個節點。
隨機從首節點到 last
節點取一個和 last
節點交換,並將 last
向前移一個節點,這裡取節點 3 當範例。
重複隨機取節點和 last
交換的步驟,這裡取節點 1 當範例。
重複隨機取節點和 last
交換的步驟,這裡取節點 5 當範例。
重複隨機取節點和 last
交換的步驟,這裡取節點 4 當範例。
當 last
指到首節點時結束,完成節點的洗牌。
commit 65c2bfc
qtest
中避免非必要的項目縮排 (即 *
, -
或數字),以清晰、明確,且流暢的漢語書寫。
授課教師在大學教書十餘年,見到不少學生在課堂表現不俗,但在面試場合卻無法清晰地闡述自己的成果和獨到的成就,從而錯過躋身一流資訊科技企業的機會。為此,要求學員書寫開發紀錄,並隨時做好「當主管問起,就能清晰解釋自己的投入狀況」,是強調讓學員「翻身」的本課程在意的事。
濫用條列 (或說 bullet) 來展現,乍看很清爽,但在面試的場合中,就很難用流暢的話語說明。反之,若平日已有這方面的練習,則面試過程中,才可充分展現自己專業之處。
首先在 qtest.c
中加入 do_shuffle()
函式,接著在 console_init()
中加入 shuffle 命令,最後在 qtest
中輸入 help 可看到 shuffle 命令已成功加入。
使用 lab0 (D) 提供之驗證程式碼進行驗證。
將初始串列設定為 [1, 2, 3, 4],並使用 1000000 次 shuffle,將每次執行的結果記錄下來。
四個節點組成的串列共有 24 (4!) 種排列方式,當執行 1000000 次 shuffle,每一種排列方式平均會出現 41666 次。
由數據得知每種排列分式出現的次數都大約在 41666 次左右,符合離散均勻分佈的期望特徵。
commit 3893eb1
避免非必要的項目縮排 (即 *
, -
或數字),以清晰、明確,且流暢的漢語書寫。
授課教師在大學教書十餘年,見到不少學生在課堂表現不俗,但在面試場合卻無法清晰地闡述自己的成果和獨到的成就,從而錯過躋身一流資訊科技企業的機會。為此,要求學員書寫開發紀錄,並隨時做好「當主管問起,就能清晰解釋自己的投入狀況」,是強調讓學員「翻身」的本課程在意的事。
濫用條列 (或說 bullet) 來展現,乍看很清爽,但在面試的場合中,就很難用流暢的話語說明。反之,若平日已有這方面的練習,則面試過程中,才可充分展現自己專業之處。
首先將 list_sort.c
和 list_sort.h
加入到 lab0-c 中,接著將 list_sort.o
加入到 MakeFile
的 OBJS 中,然後在 qtest.c
中加入 do_list_sort()
函式,且在 console_init()
中加入 list_sort 命令,最後在 qtest
中輸入 help 可看到 list_sort 命令已成功加入。
測試用 shell script,可調整佇列大小。
佇列大小 | Round 1 | Round 2 | Round 3 | Round 4 | Round 5 | 平均 |
---|---|---|---|---|---|---|
200k | 0.128 | 0.140 | 0.139 | 0.133 | 0.139 | 0.136 |
400k | 0.307 | 0.329 | 0.332 | 0.317 | 0.330 | 0.323 |
600k | 0.493 | 0.521 | 0.528 | 0.511 | 0.503 | 0.511 |
800k | 0.693 | 0.769 | 0.756 | 0.733 | 0.736 | 0.737 |
1M | 0.903 | 0.962 | 0.947 | 0.955 | 0.945 | 0.942 |
佇列大小 | Round 1 | Round 2 | Round 3 | Round 4 | Round 5 | 平均 |
---|---|---|---|---|---|---|
200k | 0.116 | 0.117 | 0.116 | 0.119 | 0.116 | 0.117 |
400k | 0.266 | 0.281 | 0.271 | 0.263 | 0.266 | 0.269 |
600k | 0.433 | 0.427 | 0.427 | 0.424 | 0.433 | 0.429 |
800k | 0.602 | 0.604 | 0.598 | 0.632 | 0.592 | 0.606 |
1M | 0.756 | 0.760 | 0.755 | 0.761 | 0.773 | 0.761 |
從資料與圖表可得知目前專案實作的 q_sort
略慢於 list_sort
,須通過研讀 list_sort
程式碼得知具體差異為何。
在佇列大小為 200k ,執行 100 次來觀察。
cache-misses : 表示程式執行期間無法在快取中找到所需資料而導致的快取未命中次數。
cache-references : 表示程式執行期間對快取的總參考次數。
instructions : 表示程式執行期間執行的指令數量。
cycles : 表示程式執行期間花費的 CPU 週期數。
觀察兩筆資料發現 list_sort
在 cache-misses、cache-references、instructions、cycles 中都小於 q_sort
,但在 cache-misses 的比例比 q_sort
要高,須通過研讀比較兩者程式的差異。
merge()
將兩個已經排序好的串列 (a
、b
) 合併為一個串列,並將合併後的串列返回。
利用 cmp()
比較 a
、b
當前節點的大小,若 a
節點小於或等於 b
節點,就通過間接指標將 a
節點接在新串列後方,反之則將 b
節點接在新串列後方,在判斷 a
、b
節點大小時若兩者相等則串接 a
節點可以保持 stable 的特性,而使用間接指標可以避免不必要的記憶體空間分配,降低空間複雜度。
此函式只會維護 next
指標,而沒有維護 prev
指標。
merge_final()
當完成所有排序操作後,將最終兩個排序好的串列 (a
、b
) 合併為一個串列,並將此串列恢復成雙向環狀的鏈結串列。
在研讀此函式時對以下程式碼感到疑惑,為什麼 if 判斷式中要使用 unlikely()
,且為什麼要比較 b
和 b
的大小?
unlikely()
在 linux/compiler.h 中 unlikely 定義如下:
此巨集會先對 x
做兩次 not 運算,因為 x
可能不是 0 或 1 的整數,通過兩次 not 運算後可以將值限制在 0 或 1 之間。
我參照 gcc 13.2 第 6.59 章說明的 __builtin_expect()
後了解此函式的功能為向編譯器提供分支的預測資訊,當 __builtin_expect(x, 0)
時預期 x
的值為 0,而 __builtin_expect(x, 1)
時則預期 x
為 1。
所以 unlikely(!++count)
此判斷式只有在當 count
值增加 1 後變成 0 (溢位) 時成立,這種情況非常罕見,所以這裡才用 unlikely
來提示編譯器此分支不常發生。
但我看到其中預設 __builtin_expect
為真的機率為 90%,不懂為什麼要以此機率當預設值。
For the purposes of branch prediction optimizations, the probability that a __builtin_expect expression is true is controlled by GCC’s builtin-expect-probability parameter, which defaults to 90%.
b
和 b
的大小因為 cmp()
會根據傳入的比較函式不同而改變,因此我查看了 lib/test_list_sort.c 的比較函式,在其中發現比較時會呼叫 check
函式。
首先此函式會先使用 KUNIT_EXPECT_LT_MSG
巨集來檢查 ela
和 ela
的 serial
是否小於 TEST_LIST_LEN
,確保他們的大小在特定範圍內。
接著使用 KUNIT_EXPECT_PTR_EQ_MSG
巨集來比較 elts
中對應編號的元素是否和 ela
和 elb
匹配。
最後使用 KUNIT_EXPECT_EQ_MSG
巨集來比較 ela
和 elb
的 position1
和 position2
是否和預設值相同,驗證內容沒有被意外修改。
除此之外根據註解的說明,如果要合併的串列非常不平衡 (例如已經排序完畢),會造成迴圈執行次數較大,此時可以在 cmp()
中定期呼叫 cond_resched()
,此函式會根據當前行程已經占用的 CPU 時間和系統中其他行程的狀態判斷是否要讓出 CPU,使其他行程不會產生 starvation。
list_sort()
通過呼叫 merge()
和 merge_final()
對串列進行排序。
其中最不理解的部分為迴圈內判斷是否該進行合併的程式碼。
首先 for 迴圈會通過位元運算來找到 count
最低位的 0,並同時更新 tail
指標,當 count 為 2 的冪加 1 時,就會執行一次合併,這樣是為了確保最多只有兩個子串列在等待合併。