contributed by < youjiaw >
作者在組建團隊時找了不同科系的人幫忙,這部分談到了學用落差的現象,「資工系的學生不會寫程式,機械系的學生不會做機械」,儘管大學安排了很多紮實的課程,但學生的實做能力依舊貧乏。而我也曾有過這方面的困惑,例如,修這些課程會有幫助嗎?我未來什麼時候會用到?
但從另一個角度來看,如果環境就是這樣,那學生是否更應該重視自主學習,做一些與其他人不一樣的事?像是在完成專案的過程中,作者利用了嵌入式系統課程與大學期間寫網站的經驗寫出飲料機的程式,其他成員也因為跑工廠、花時間做很多習題累積了不同的經驗,從而有能力為專案作出貢獻,這些皆是曾經的犧牲所換來的結果。
然而,除了如何將自己所學好好發揮出來,也許更重要的是如何面對陌生領域的種種問題,
「你不能現在就放棄,要是現在就放棄的話,你這輩子日後遇到這種等級的困難,就只會想逃避而已。」
這是我看完之後最有感觸的一句話,作者沒做過機器,卻經手了飲料機的每個細節,即使經歷了多次的自我懷疑,作者在遇到各種問題時都沒有放棄,而是持續學習並不斷嘗試與改進,這樣的精神讓我十分敬佩。同時,作者通過不斷確認自己要什麼、不要什麼,做出取捨讓專案可以在自己的底線內達到目標,這也呼應了先前提到的,為了達成目標,有時候必須做出取捨和犧牲。
我發現我就像老師說的,所有科目考完試就忘了不少,因為過去我的學習動機都是為了在考試中拿取高分,從而達到其他目的,而不是真正理解學科內容。這樣的習慣也讓我發現「記錄疑惑」對我來說是有難度的,因為我已經習慣被動的接收那些考試內容,而不是主動思考、質疑答案的真實性。
因此,這堂課讓我重新審視了自己的學習態度,雖然我基礎知識缺了不少,導致閱讀教材和寫作業花的時間比我預期的還要多,但正如老師所說的「誠實面對自己,缺什麼就補什麼」,如果逃避,問題還是在那裡,努力想辦法解決問題才是正確做法。
branchless
https://hackmd.io/@sysprog/binary-representation
security
CPU pipeline: hazard
stall
out-of-order execution
ILP: instruction-level parallalism
instruction latency
system (國中物理)
load balance => CPU scheduling book
TODO: https://hackmd.io/@sysprog/concurrency 並紀錄問題
TODO: quiz9,quiz10,quiz12 (or others related to concurrency)
alignas
可以避免 false sharing 或增進記憶體的存取效率,但是我查閱 Linux 核心原始程式碼卻沒找到此操作。隨後發現 Linux 核心是使用 attribute((aligned(sizeof(long))))
這種對齊方式。fork_count
並不共享於 process 之間。因此如下圖,執行第一次 fork 之後 Task 1
和 Task 2
會在各自的記憶體區域紀錄 fork_count = 1
,導致再進行 fork 後,紀錄的 fork_count
為 2,但是實際 fork 的次數已經是 3 次了。因此 fork_count
應是記錄 fork 的深度?教材詞彙修正:
FUTEX_WAIT_PRIVATE
的 FUTEX_WAIT
」,option 是否應更改為 operation?futex
中所有的等待者超過 limit
個,則other
這個 futex word 對應的 wait queue 中等待」,其中”所有的”是否應更改為“剩餘的”?my_v = v
與示範程式碼 bv_v = v
不符(my_v = v
為第一章的示範程式碼)。bpp
相關函式與變數,及 tpool.c 第 324 行的註解 BPP
,是否應更正為 bbp
?執行人: youjiaw
專題解說錄影
以第九週測驗一給定的 concurrent (fork) merge sort 為基礎,嘗試實作2024-04-16 問答簡記中,上課時說明的可能改進方法。
原版 merge_sort
函式:
改進方法:
參考〈案例: Thread Pool 實作和改進〉的 tpool.c,加入 thread pool。
首先更改 main
函式,在呼叫 merge_sort()
之前先於 thread pool 中建立指定數量的 thread,並在排序完成後將資源釋放,減少動態建立和釋放的開銷。
merge_sort()
函式先將任務依照中間點,切分為左半邊與右半邊,並以 arg
結構記錄任務的變數,接著使用 tpool_apply()
將各任務要執行的函式與變數加入 workqueue 中。
最後等待任務執行完,就釋放相關資源,並呼叫 merge()
。
其中,新增的 arg
結構體與 merge_sort_task()
函式如下。
但是在這樣的更改下,程式無法順利執行完成。經過測試發現,所有 threads 都會在卡在 merge_sort()
第 16 行的 tpool_future_get()
中,呼叫 pthread_cond_wait(&future->cond_finished, &future->mutex)
等待子任務的完成,卻一直沒有被喚醒。
進一步分析發現,因為 thread 是透過 jobqueue_fetch()
來取得並執行任務(下方第 1 行),而上述等待情況所對應的喚醒函式 pthread_cond_broadcast()
需要等 merge_sort()
執行完成後才會被呼叫(下方第 11 行),但 merge_sort()
本身又在等待子任務完成,形成了所有 thread 執行的任務都在等待子任務完成,這種無法結束的等待情況。
因為目前沒有想到其他的解決辦法,所以直接將 merge_sort()
改為非遞迴的版本,來避免任務與子任務之間的等待狀況。
非遞迴版本中,要交給 thread 執行的任務就沒有 merge_sort()
了,只有 merge()
,程式會在 merge_sort()
中提前切分好所有要進行 merge()
的任務,並將這些任務加入 workqueue,更改後的 arg
結構體與 merge_task()
函式如下。
merge_sort()
中,等待 future
任務執行完成與釋放資源的函式,可以選擇放在兩個迴圈之間,或是放在最內層的迴圈。因為若是如下方放在迴圈外面,除了 futures
陣列大小難以決定以外,一次性的把所有任務加入,也會導致執行順序無法確定,造成結果錯誤的狀況。
如下方 N_ITEMS = 4
的例子,workqueue 會被加入第一層的兩個任務,及第二層的一個任務,假設 thread 數量為 3,且各取得了一個任務,這時如果第二層的任務比任何一個第一層的任務早完成,排序結果就可能會出錯。
如果將函式放在兩個迴圈之間,相當於每次都只先加入一層的任務,並立即執行所有任務,然後再繼續加入下一層的任務,直到結束。這樣就可以保證上述不確定的情況被排除。
若是將函式放在最內層的迴圈內,則是代表每放入一個任務,就等待他做完才放入下一個任務,失去了並行的能力。
但是我猜測這種方法可以讓剛加入的任務還在 Memory hierachy 上層時,就先進行合併,所以對 cache 較友善,而且也不需要多餘的記憶體空間來存放 futures
陣列。
使用以下兩種方式進行評估:
clock_gettime()
來測量,避免 cpu pinning 以及同步的問題。
This use of RDTSC for timing suffers from these fundamental issues:
Discontinuous values. Using RDTSC directly assumes that the thread is always running on the same processor. Multiprocessor and dual-core systems do not guarantee synchronization of their cycle counters between cores.
…
以下測試皆固定建立 10 個 thread。
首先比較上方提到的,將等待任務執行完成與釋放資源的函式放在兩個迴圈之間(以下簡稱 A 方法),或是最內層的迴圈(以下簡稱 B 方法)。
執行時間
使用 clock_gettime()
測量 10 種陣列大小,從 100,000 筆資料開始,每次加 100,000 一直到 1,000,000,並且每個大小都會重複執行 5 次,最後再取執行時間的平均。
可以發現 A 方法在所有資料大小的平均執行時間都比 B 方法快 3 倍左右。
Perf
使用 perf stat
分析 cache-misses, cache-references, instructions 以及 cycles。
觀察下方結果可以發現,有如先前所推測的,B 方法雖然執行速度較慢、花費的 instructions, cycles 比較多,但是 cache-misses 的機率從 3.66% 下降到了 0.21%。
這邊我疑惑的是,為什麼 instructions per cycle 比較多?
A 方法:
B 方法:
經過查詢,cache-misses 與 cache-references 計算的是 last level cache,再次測試 L1-dcache 發現 B 方法的存取次數多了快 5 倍。
A 方法:
B 方法:
我原先認為的 B 方法因為一加入任務就執行,所以 L1 cache 存取次數會比較多、misses 少,且 last level cache 的存取次數較少,但與測試的結果不相符,目前還沒有想到原因。
至於為什麼具有較好並行處理能力的 A 方法 instructions per cycle 會較低,我原先的推測是 thread 之間的切換造成較多的 context switch,但是測試結果如下。
A 方法:
B 方法:
所以整體而言,A 方法無論是在執行時間,還是資源的消耗都明顯優於 B 方法,而 B 方法的 instructions per cycle 略高的原因,我會再繼續研究。
原程式碼的 fork_count 達到限制次數之前,每次 fork 都會增加當前的 process 數量,代表最多有 2^n 個 process 同時運行。所以我認為可以建立相同數量的 thread 來進行比較。
經過測試發現,原程式碼的執行時間和使用的資源並不會隨著 fork 數量的增加而有顯著的改變,但是經過我的修改,卻變成所有開銷都會隨著 thread 數量增加而變多。
原程式碼:
我的程式碼:
原始 fork 版本效能較好的可能原因:
我的版本效能較差的可能原因:
我會繼續探索這些問題並嘗試更多優化方案,以進一步提升 concurrent merge sort 的效能。