contributed by <Nsly0204>
在了解宣告的組成之後繼續翻閱規格書,了解到因為 fasr 只會走訪點,並不會對節點的值進行更改,在此情況下宣告成 ptr_to_constant 可以避免 fast 指標不小心更動所指到的變數。
其中的fasr
打錯字
在 harness.h 中可以找到被定義的巨集,兩種方式其實是一樣的,特別定義成 test_free 目的是為了做之後的記憶體用量偵測。
t_free
中,test_free
用來檢測記憶體用量的部分在harness.c中,透過allocated_count--
來追蹤當前已分配的區塊數量,還增加了檢測記憶體損壞
參考為什麼該接觸 GNU/Linux 開發工具,紀錄下之後可能會用到的語法。
以下命令的更動可以在git ~/.gitconfig
找到
撰寫.gitignore
可以忽略特定不需要被 commit 的檔案,下git status
時也會一並忽略。
如果在 commit 也想對不同的檔案版本做分類,可以從主幹master
創建分支(branch),在個分支中的commit互相獨立,所有對檔案的更改只有在合併分支(merge)的時候合併。
HEAD
可以想成一個指向 commit 的指標,所有的git
操作都會在HEAD
指向的 commit 後面做延伸,包含merge
、branch
、rebase
等,詳細內容可以看Git 教學系列 - Branch and Merge。Fast Foward (ff)
在執行 merge 時若系統偵測到目前 HEAD 所指向的分支不須更動即可以 merge 這時系統在執行 merge 時會自動跳過 commit 的階段,稱之為 fast forward 。若要避免可以下$ git merge <branch> --no-ff
當合併分支的時候會顯示出檔案中有衝突的地方,每個有衝突的區塊會用以下的形式標示:
下git merge <branch>
時會要求把所有有衝突的地方合併,亦即刪去<<<<<<< HEAD
、=======
、>>>>>>> branch
、和所有不需要留下的程式碼。編輯完之後可以到git status
查看待 commit 檔案狀況,最後再commit即完成分支的合併。
rebase
與merge
都需要解決不同 branch 中檔案版本發生衝突(conflict)的情況,主要的差別在於後者不能對 commit 進行修改,能做的是僅僅根據現有的 commit 對檔案內容進行修改而後合併,注意到這邊的合併(merge)需要將最終的檔案版本進行額外的 commit ,相反rebase
主要的用途是移動 commit ,而 rebase 途中遇到衝突只需要直接修正檔案<<<<<<< HEAD ======= >>>>>>> branch
部份即可,不需要額外的 commit。
$ git rebase -i
是非常強的功能,主要協助開發者進行 commit 的管理,功能涵蓋修改順序及內容、合併或分開 commit 等等。
由於$ git rebase
功能的強大,同常在進行該操作時有以下規範:
$ git rebase -i
涉及 commit 歷史的改變,已經公開到遠端的 commit 不建議更改$ git rebase -i
$ git rebase -i
這裡舉一個在我尚不理解 git 是怎麼運作也不了解其各命令所進行的操作時遇到的困難:背景是我已經在本地端打了一些程式碼,有一些已經 commit 有一些還沒,而在已經 commit 過得程式碼中更有分為有git pull
到遠端和沒有到遠端的。
此時我打開 github 發現有sync fork
的驚嘆號,想都沒想就按下去之後一路選擇預設選項,結果就是因為 git 無法同步我的程式碼而直接詢問我是否捨棄當前的 commit ,而我也大膽的選下了Discard
。
然後我到本地端就接收到了pull request
,下命令後就得到了以下的報錯:
查看git status
得到以下說明:
可以看到我電腦本地端有一些尚未 commit 的程式碼,但由於剛才的sync fork
讓我刪除了遠端的 commit ,造成遠端、本地 commit 、本地未 commit 三種版本不相符,造成我無法git pull
、也無法git commit
。
後來學習了git rebase
之後,先把本地端所有程式碼完成 commit ,再$ git pull --rebase
成功把現有的本地 commit 嫁接到遠端commit 後面。
在開始偵錯之前我們通常需要用 break
設定中斷點, run
開始執行除錯,list
展開程式碼方便觀察和操作。
若要查看 object 在記憶體中所佔用區域的起始位址可以使用print &object
的指令,注意這只包含現在 scope 可見的變數,這也回應了我們設定中斷點的用意。更多可以參考 GDB print 詳解。
了解 linux 核心中對於值的讀取和寫入都需要使用巨集WRITE_ONCE
READ_ONCE
背後的原理及考量。
在list_for_each_entry_safe
中用到了container_of
巨集,閱讀Linux 核心原始程式碼巨集: container_of 後了解到 linux 核心如何利用 offsetof
反向找出包含目標成員結構體的地址,這可以說是 list_head
結構體的精髓,讓我們可以藉由把 list_head
嵌入到另一個結構體,但還是能使用現成 list.h
中定義的所有函數和巨集做到如 traversal 、 swap 、 delete 、 sort 等方便且高效的函數,我想這也是本次作業的目的。
可以看到程式碼中使用了 test_free
和 free
兩種釋放記憶體的方式:
在 harness.h
中可以找到被定義的巨集,兩種方式其實是一樣的,特別定義成 test_free
目的是為了做之後的記憶體用量偵測。
這裡參考了 willwillhi 和 vax-r 實作 merge sort(stable sort),在靜態檢查時遇到錯誤:
到這裡我發現我對C語言中變數的宣告組成的無知,於是查閱 C11 § 6.7 Declarations
了解到基本的宣告其實可以歸列成以下形式:
§ 6.7 Declarations
—Syntax—
declaration-specifiers:
declaration-specifiers init-declarator-list
init-declarator-list
可以想成一個或是一連串的變數名稱(更清楚的說是 identifier ),而其中 declaration-specifiers
包含 storage-class-specifer
(e.g. static, etc.) 、 type-specifier
(e.g. int, char, etc.)、type-qualifier
(e.g. const, etc.) 、 function-specifier
(e.g. inline, etc.) 、 alignment-specifier
。
在了解宣告的組成之後繼續翻閱規格書,了解到因為 fasr
只會走訪點,並不會對節點的值進行更改,在此情況下宣告成 ptr_to_constant 可以避免 fast
指標不小心更動所指到的變數。
§ 6.7.6.1 Pointer declarators
考慮以下兩種宣告形式:
const int *ptr_to_constant;
int *const constant_ptr;
解釋:
The contents of any object pointed to by ptr_to_constant shall not be modified through that pointer,
but ptr_to_constant itself may be changed to point to another object. Similarly, the contents of the int pointed to by constant_ptr may be modified, but constant_ptr itself shall always point to the same location.
在沒有任何背景知識下,參考〈解讀計算機編碼 - 第三部分:在資訊安全的應用〉了解時序攻擊法 (Timing attack) 是旁路攻擊 (side channel) 常見的一種攻擊方法,主要透過偵測程式的運行時間獲取有用訊息,也就是所謂的資訊洩漏 (leakage)。
論文提供的常數時間測試可以分成三個步驟,分別是 Measure execution time / Apply post-processing / Apply statistical test ,以下是論文摘錄:
Measure execution time
preparation task
被放在時間測試之前。Apply post-processing
Apply statistical test
Welch's t-test
又名【不同變異數獨立樣本平均值檢定】,目的為比較兩個不同取樣的分布是否具有相似的統計特性。顧名思義,相對於取樣來自常數分布的 Student's t-test 在面對母體具有不同變異數的常態分布時更可靠,這也是本次實作採用 Welch's t-test 的考量。
程式碼實作
Commit 8ee635d 先在資料夾中新增測試用檔案,方便以後下 $ python3 test_shuffle.py
進行測試。
新增新 command 前參考 lab0 (三) 搞懂各個檔案的關係,了解到 console.[ch]
負責 command-line interface 的實作,其中就包含了新增命令的函數:
在 init_cmd()
的地方可以看到預設新增未知指令#後會直接呼叫 do_comment_cmd
的函式,由此可知我們新增命令需要三個步驟:
commit e498635
現在我們知道需要先依照 console.[ch]
規定的方式在 q_test 新增命令:
實作 do_shuffle
由於所需要的輸入和返回型態都一樣,直接參考 do_ascend 進行實作:
實作 q_shuffle 函數
根據作業要求,使用 Fisher-Yates shuffle algorithm 進行實作。
本段 shuffle 程式碼學習 weihsinyeh 精簡程式碼的方式,但在執行後發現無法達成特定排列,後來發現是因為 rand() % (cnt--)
對應到的值域是 [0,N-1] 但我們所需要的位移次數是 [1,N] ,故做了以下修正:
後來新增 Commit 3796bda 把 q_shiffle 移到 q_test 解決 queue.h 不能被更動的困擾。
重複實驗後發現 chi square sum 大多落在區間 [15, 35],參考自由度等於所有組合度 24 的卡方分佈表得到 P_value 屬於 [0.1, 0.9] > = 0.05 。統計結果沒有達到顯著水準 ,不具有統計顯著性,因此無法拒絕 shuffle 結果為 uniform distribution 的虛無假說(H0)。
研讀完 lab0 (E) 後,可以發現這真的是一段精簡且強大的程式碼。以下我會先依照我的理解說明程式大綱,並歸列出幾個我認為重要且值得學習的地方進行說明。
可以發現lib/list_sort.c
在執行 while 迴圈時把所有節點分為三類:
根據原程式註解分析從小排到大的流程:
a = merge(priv, cmp, b, a);
a->prev = b->prev;
使子串列最後一個節點的 prev 指向 NULL,又因為是從長度為1的子串列開始合併,因此到最後會把所有已排序的子串列節點的 prev 都指向 NULL。*tail
移動到合併後子串列的 head ,好習慣確保 tail 不會指向空指標。摘要
for (bits = count; bits & 1; bits >>= 1)
可以發現程式碼巧妙的發現等長度合併的過程其實就是一種二進位,待合併的串列長度等同 count
最低位的1代表的值,將計算 tail 移動的過程用位元運算捨棄四則運算。prev
、next
不同的資料結構意義。if (!a) { *tail = b; break;}
相對簡單且有效率,具有"品味"。詳細探討合併規則
為了理解合併邏輯我去參考 lib/list_sort.c
的註解:
This mergesort is as eager as possible while always performing at least
2:1 balanced merges. Given two pending sublists of size 2^k, they are
merged to a size-2^(k+1) list as soon as we have 2^k following elements.
可以看到目標是在第三個 出現的時候馬上合併,呼應老師的講義
合併方式是當有 個 節點時,合併前兩個變成 ,並留下一個 不動,維持著合併:不合併為 2 : 1 的比例,因為只要 可以放得進 L1 cache,可以避免 cache thrashing。
從老師在 lab0 的註解,可以猜測與 cache trashing 有關,已知 merge sort 在兩兩長度相等時存在最佳效率,參考以下不用進位的例子。
count 變化 | count 二進位 | merge | pending 上的節點 |
---|---|---|---|
15 → 16 | 1111 → 10000 | no (2⁴) | 8 ← 4 ← 2 ← 1 ← 1 |
想像今天存在一個最大容量為15個節點的快取,最晚發生 page fault 的節點情況為8 ← 4 ← 2 ← 1
,其中此快取內無法再進行任何等長度合併,而為了達成這種類似堆積木的結構(以下簡稱堆疊),2:1的合併規則因此誕生。
然而很簡單的可以發現現有的合併方法並非唯一能達成此種堆疊的合併方法,舉例來說,可以在每次新增節點時,都去搜尋所有可以等長合併的子串列,如下所示:
count 變化 | count 二進位 | pending 上的節點 |
---|---|---|
0 → 1 | 0000 → 0001 | 1 |
1 → 2 | 0001 → 0010 | (2) |
2 → 3 | 0010 → 0011 | (2) ← 1 |
3 → 4 | 0011 → 0100 | (4) |
4 → 5 | 0100 → 0101 | (4) ← 1 |
但這種合併需要花時間尋找待合併子串列的個數和位置合併效率明顯低落。