contributed by < gaberplaysgame
>
在終端機執行 export LC_ALL=C
,再執行上述命令。lscpu
套件目前的中文翻譯品質不佳。
注意書寫規範,中英文間用一個半形空白字元區隔
依據作業規範,q_new()
應配置可容納 list_head
的記憶體空間,並對節點進行設定,這部份list.h
有INIT_LIST_HEAD(head)
可以做到,故這裡就直接用內建函式處理。考慮到 malloc 有可能無法配置空間,所以用一個if處理空間分配失敗的狀況,若失敗則 head 為 NULL 。
與下面的 insert_tail 是同樣道理,分了三次 if 來去排除掉可能 malloc 錯誤或者 head 為 NULL 的情況。
注意書寫規範,中英文間用一個半形空白字元區隔
這邊利用內建的函式來做 free ,用 for 迴圈遍歷 –- 整個鏈結串列,複雜度為 O(n)。
注意 資訊科技詞彙翻譯 並改進漢語表達。
與下方的remove_tail
是一樣的,只是改head->next
或head->prev
。
利用 Floyd Tortoise and Hare Algorithm ,原理是給定兩個迭代節點分別跑一步與兩步,若兩個節點同時在除起點外同個位置時則代表鏈結串列有閉環。因為 linux 的鏈結串列是用循環雙向的方式做的,因此可以利用該演算法,在 Hare 第一次跑到鏈結串列的尾端時,Tortoise 會剛好在中間節點。
由於鏈結串列的長度會有分單雙數的情況,單數時 Hare 可以剛好跑到尾端,雙數則會多跑一格 ?? 到 head 指標。由於 Hare 一次跑兩格,所以跑到尾端和跑到 Head 的事件不會同時發生,可以當作 while 判斷式的中止條件。
注意量詞,翻閱辭典以理解「格」的使用時機。
原本寫的程式碼:利用一個flag
和strcmp
進行兩個元素的比較,若strcmp
出來0則flag
為 true 並刪除cur
的元素,下次若是strcmp
非0,flag
為 true 時亦會刪除元素。如此便有三點要處理:
next == head
時,由於head
沒有數值,不能進行strcmp
,因此只能進到flag
判斷。strcmp == 0
時,進行元素刪除。flag == true
時,進行元素刪除。下面是第一版的程式碼(節錄):
可以看到上面程式碼用了很多 if,尤其list_del
的部份有重複,自己都認為沒有品味,因此花了一些時間在改進程式碼方面。
首先嘗試了把上面提到的重複部份整理一頓,先忽略了next->list
可能為head
的情形,利用cmp
與flag
兩個數值相反的特性將區塊合併。
flag
會被設為 true ,這代表如果下一圈 cmp 不是0時仍可以刪除 cur 的元素排除掉未刪除元素的可能性後,可以看到flag = !cmp
的性質,且可以放在程式最後面執行,因此只要把 cmp 非0,flag 為 false 的情況先擋掉,就可以把兩個區塊合併。
再來就是解決next->list
可能為 head 的情形了。若此狀況發生,cmp 沒辦法決定值,因此給這段加上了一個三元判斷式判定若該狀況發生,指定 cmp 為 true 。如此可以保證if (cmp && !flag)
能被觸發,再來若 flag 為 true 可以保證元素能被刪除。
改進過後的程式碼:
不用張貼完整程式碼,只要列出差異的部分,善用 diff
element_t *cur, *next;
bool flag = false;
list_for_each_entry_safe (cur, next, head, list) {
int cmp = &next->list != head ?
strcmp(cur->value, next->value) :
true; // no comparison if next is head,
// comparison gives 0 or nonzero value
if (cmp && !flag)
continue;
list_del(&cur->list);
q_release_element(cur);
flag = !cmp;
}
return true;
}
目前只有這個函式還沒有去做正確性的測試,等sort與reverse等做完後才能進一步確認是否運行順利。 經過測試後確定可以順利通過autojudge測資。
注意看程式碼,lab0-c
使用 autograde
一詞,「測試案例」不該簡稱「測資」。
因為下方的 reverse 函式會用到 swap 的技術,所以將實際將兩個節點進行交換的步驟給獨立出來。
改進你的漢語表達。
本意上用到的是頭尾交換的想法,由於是循環陣列容易找到鏈結串列尾,故用這個方法實作。
方法目前只有想到類似硬破法,將佇列拆為 k 個(或更少)一組,然後分別進行 reverse 操作。
這裡的 sort 是使用 mergesort 來實作,方法有參考你所不知道的C語言:Merge Sort的實作與間接指標的應用,但有用到將循環鏈結串列斷開成一般鏈結串列的操作,還沒有想到有沒有可以保持循環鏈結串列的實作方式。
有用到前面 merge 的想法,並且用 Devide & Conquer 的方式處理合併,使時間複雜到可以降到 O(NlogK)。
注意書寫規範,中英文間用一個半形空白字元區隔
這函式用了兩個指標 left
與 right
來處理數值比較,當 right 指到 head 時結束操作並回傳佇列大小。其餘實作方式基本可以參考 remove 系列。
注意書寫規範,中英文間用一個半形空白字元區隔
注意書寫規範,中英文間用一個半形空白字元區隔
引入linux/list_sort.[c, h]
並做了些特別的修改,主要是為了繞開不能修改 queue.h
的限制,所以將 list_sort.[c, h]
作為新的引入檔,以 q_list_sort 作為在 qtest.c
的呼叫函式。
cmp 函式以此方式實作,由於不太確定 priv 傳入值的用處,加上可以為 NULL 值,cmp 函式就將此刪除:
這邊以原本進行 make 是沒有問題的,但在更新 GitHub 時有遇到 cppcheck 的 "Null pointer dereference" ,指向 list_entry 的用法。
起初不太知道這個錯誤應該如何解決,以為是程式碼本身的錯誤,在這個程式碼上面花了一點時間。觀摩 jasperlin1996 後發現說可能是 cppcheck 的問題。由於學長已經做過了 cppcheck 1.9~2.7 的測試,自己把最新版的 2.10 安裝之後這個錯誤仍存在,最後只好使用 cppcheck suppression 的方式解決,即使用 cppcheck-suppress nullPointer
繞過。這邊引用學長對於這個錯誤的敘述以及當屆討論:
這邊是因為 list.h 中 container_of 的實作中,有一行是 typeof(((type *) 0)->member) 而 (type *) 0 的確是 null pointer。不過這邊的用法是為了取得 member 的 type,且這個用法是符合 ANSI C 的標準的,詳細的說明可以看這篇:https://stackoverflow.com/questions/13723422/why-this-0-in-type0-member-in-c
以及規格書的第 6.3.2 章關於 pointer 的說明:http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf
另外直接引入 list_sort.[c, h]
會遇到 likely & unlikely 巨集的問題,雖然這段編碼只是為了優化 gcc ,但為了不過度影響程式效能自己還是從compiler.h
抓入相關部份並放入list_sort.c
。
注意書寫規範,中英文間用一個半形空白字元區隔。
改進你的漢語表達。
利用 Perf 進行效能分析,執行 10 次並取平均,測試程式如下:
Perf 似乎無法支援筆電 CPU ,導致部份如 instruction, cycles 的測試無法進行,這邊先貼上部份的測試結果,之後預計會換回桌機進行測試。筆電使用 i7-7700 CPU 無法進行有關 instruction 與 cycles 等的測試,換回桌機使用 i5-11400 已解決問題,故以下會使用桌機數據重新進行紀錄。
清楚列舉硬體型號和相關系統資訊,這樣他人才能協助排除問題。
q_sort
,l 為 q_list_sort
number | instruction | time | cycles | page fault | branch |
---|---|---|---|---|---|
1024_q | 5,934,943 | 0.91 ms | 3,884,975 | 112 | 911,195 |
2048_q | 10,164,886 | 1.48 ms | 6,164,264 | 150 | 1,509,771 |
4096_q | 18,852,459 | 4.76 ms | 19,931,933 | 220 | 2,755,409 |
10240_q | 45,017,257 | 6.47 ms | 27,146,322 | 438 | 6,533,543 |
102400_q | 448,797,359 | 100.75 ms | 425,198,490 | 3677 | 65,891,213 |
409600_q | 1,824,311,621 | 649.45 ms | 2,562,835,035 | 14477 | 270,998,617 |
1024_l | 5,504,024 | 1.76 ms | 3,135,236 | 112 | 797,843 |
2048_l | 9,005,109 | 2.62 ms | 10,824,548 | 148 | 1,229,047 |
4096_l | 16,449,283 | 6.55 ms | 12,042,236 | 221 | 2,164,575 |
10240_l | 38,676,693 | 18.08 ms | 18,684,006 | 437 | 4,955,814 |
102400_l | 372,969,154 | 46.82 ms | 197,921,647 | 3677 | 46,956,545 |
409600_l | 1,488,046,316 | 177.62 ms | 725,989,221 | 14477 | 187,069,862 |
這邊取的是平均值的數值,實際的時間差在正負 10ms 左右。可以看到在 instruction, cycle, branch 上 list sort 都比自己寫的 sort 還有好。由於時間差落在正負 10ms ,因此在數量級小的情況下兩個演算法難分軒輊,但若是數量級大則可以明顯看到 list sort 的表現比自己寫的還要消耗更少時間。綜合以上數據可以發現 list sort 在各方面都優於自己實作的 merge sort 。
執行make valgrind
可以得到以下結果(節錄):
這邊取trace-01-ops
的問題進行細部觀察,可以看到大致由三種問題組成:
第一、二個問題源自 bufsize 大小為 1025,然而測試字串 dolphin 的大小長度只有 8 ,所以在 memcpy(sp, remove->value, bufsize);
中會造成記憶體空間錯誤。所以只要將 bufsize
更改為bufsize > strlen(remove->value) + 1 ? strlen(remove->value) : bufsize
就好。
根據執行順序,判斷第三個訊息來自於把佇列清空後沒有正常釋放記憶體所導致,根據q_quit
函式可以看到有個不正常的return true
在程式第3行被執行,導致後續沒有進行 free 佇列。將不正常的程式碼移除後即可解決這個錯誤。
再次執行make valgrind
可以看到問題皆以修復。
這邊設計一個實驗組與一個對照組,採用的是前章節出問題的 removehead 指令 –-,可以得出兩種結果,前者為優化前,後者為優化後:
去查詢 optimize 的意思,留意 optimal 的定義。不要濫用詞彙。
利用valgrind --tool=massif ./qtest -f traces/sim-rh.cmd
與 Valgrind User Manual 9.3 的 massif-visualizer 來顯示結果:
可以看到在針對 q_quit 函式進行修復後,有改善記憶體的釋出,但程式執行最後仍未完全釋放記憶體。
使用 Address Sanitizer 後進行 make test
後發現編號第 14 的測試案例因超時無法順利通過,而編號第 17 的測試案例中,常數時間測試在 remove_head/tail 時為常數時間時非常數時間。比對過其他人程式碼後還沒有找到問題在哪裡,預計會再進行深入研究。
不要說「其他人」,明確列出你參照的帳號名稱,還有你如何實驗。
command 的翻譯是「命令」,instruction 的翻譯是「指令」,二者語境落差大。
參考 Fisher-Yates Shuffle 演算法的偽代碼 虛擬碼進行實作。
台灣的慣用翻譯風格中,少用「偽」,反過來說,中共的刊物就常見。
實作完成後利用範例的測試程式進行卡方()的計算,得出約等於 22.9521 。 的排序中共有 24 種排列組合,故自由度(P)為 23 。根據卡方分布圖查詢可知 P-value 介於 0.25~0.5 之間,大於常見的顯著水準 alpha (0.05) 。統計結果鑑定不拒絕虛無假說。