# Mergesort 效能分析 contributed by <`fzzzz`> , <`nekoneko`> , <`abba123`> ###### tags `fzzzz` `nekoneko` `abba123` `sysprog` >>中英文關鍵字之間請記得使用空白隔開 >>[color=red][name=課程助教] ## 目標 - [x] 試圖找出之前實驗時,數據抖動的原因 - [x] 分析 lock contention 對於執行時間的影響 - [ ] 整理semaphore, mutex, condition variable資料與比較三者差異 - [ ] 了解何謂coarse locking ### 找出數據抖動原因 在[原本的實驗](https://hackmd.io/s/SJfa8wFR)中,我們測試的時候發現執行時間會有不規律的抖動情況出現,即便是使用 gnu 內建的 `__buildin___clear_cache()` 仍然會存在著不規律抖動,所以我們推測,該抖動的來源可能是來自各 thread 的有效利用率不足。 歸類出以下兩種 * 有效執行的 task 在各 thread 中分佈不均勻 * lock contention 導致大量 thread 處於等待鎖釋放的階段 #### 單一數量執行緒測試 ``` for i in `seq 1 1 50`; do ./sort 100 input.txt; done ``` ![](https://i.imgur.com/vLtEmmy.png) ![](https://i.imgur.com/I96Cr1T.png) ### 各thread執行有效task的分佈 ![](https://i.imgur.com/t2HTrmN.png) >> 希望能探討分佈平均與否對於效能的影響 [name=`fzzzz`] >> 多次實驗後發現沒有太大的影響(利用variance來作為分散程度的表示方法) >> 等等補圖 [name=`fzzzz`] #### 分佈均勻度對時間影響 ![](https://i.imgur.com/59bc0MK.png) 就實驗結果看來在大量執行緒時,分佈均勻與否並不影響執行時間 ### 將mutex改成POSIX semaphore的實做 將main.c和threadpool.\*的mutex改成POSIX semaphore - benchmark ![](https://i.imgur.com/1KegsWL.png) - 各 thread 執行有效 task 的分佈 ![](https://i.imgur.com/5m6ZMwD.png) > semaphore 與 mutex 的差別,有需要再讀資料了解[name=cheng hung lin] > 信號標和鎖的不同,鎖會有擁有者(task),semaphore 的話比較像信號標,來告知可不可以過[name=`fzzzz`] >> 上面那行到底在寫什麼?拜託清楚解釋差異,不要用不清不楚的詞彙來自欺欺人 [name=jserv] ### 利用 mutrace and gprof 來找出效能瓶頸 **mutrace** ``` Mutex # Locked Changed Cont. tot.Time[ms] avg.Time[ms] max.Time[ms] Flags 0 3440715 2706841 1711166 461.433 0.000 4.445 Mx.--. ``` **gprof** ``` Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls ms/call ms/call name 43.83 0.07 0.07 2805641 0.00 0.00 tqueue_pop 43.83 0.14 0.07 198755 0.00 0.00 merge_list 6.26 0.15 0.01 349900 0.00 0.00 list_add 6.26 0.16 0.01 main 0.00 0.16 0.00 395224 0.00 0.00 list_new ``` 經多次測量並觀察後發現,mutex contention 次數愈多的,執行時間會愈久,tqueue_pop 的呼叫次數也會變多 但 function 的呼叫次數與 contention 相比, contention 的影響是大於無意義的 function call 原因是發生 mutex contention 時,該 thread 會被卡在同一個地方,直到 lock 擁有者釋放 lock 和組員討論過後的第一個想法是使用 condition variable,讓不必要的 thread 進入 wait 的狀態(此時該執行緒就不會使用任何 CPU 資源) 這樣就不會浪費 CPU 資源再多次嘗試獲取 mutex 上 --> 使 mutex contention 大幅下降 --> 各 processor 有效執行效率也會變高 >> 晚點補上測資 [name=`fzzzz`] 剩下的問題就是若改成 lock-free 的版本的話,效能是否更進一步提升? >> 在[andy19950的共筆](https://hackmd.io/s/rJ6m3IvC#future-work)裏有提到改成 lock-free 卻無法提升效能 >> 目前想法是,問題應該是出在沒意義的 function call 就變得更多了(待驗證)[name=`fzzzz`] >> 使用`htop`指令來看 CPU 的使用量,可以發現 mutex 版本的 cpu 使用量會是滿載,而 condition variable 的狀況卻是約一半(每個core)[name=`nekoneko`] >> 這個程式主要探討 lock 的影響,thread queue pop 操作會佔用大部分 CPU 資源的原因是,內部不是採用 coarse locking [name=jserv] ### 利用 condition variable 的實驗 以下測試皆為利用 mutrace 對128個 thread 做100次追蹤的結果 ``` mutrace ./sort 128 input.txt ``` :::info 使用到128個執行緒的原因是,我們想觀看大量並行的效率如何 ::: #### 原版的 contention 數量與執行時間的關係 ![](https://i.imgur.com/YJFg703.png) >> 基本上是正相關,但有些許飄動可能是 clock_gettime() 所導致的浮動 #### 換成使用 condition variable 的版本 ![](https://i.imgur.com/naIEVSn.png) >> 這邊的 contention 數量愈多時間基本上還是會上漲,但會上下飄動,理由推斷是 thread 喚醒時的 contex switch 時間不一 [name=`fzzzz`] ### 資料分佈 #### 假設 - cut_func是將link list的資料切成左半llist和右半llist,再透過push task到work queue將資料交給下一個cut_func做處理。若當傳進去的llist 長度為1或是cut_count大於等於max_count時,cut_func則呼叫merge做單個thread mergesort和thread與thread之間的merge。 ```c=103 void cut_func(void *data) { llist_t *list = (llist_t *) data; pthread_mutex_lock(&(data_context.mutex)); int cut_count = data_context.cut_thread_count; if (list->size > 1 && cut_count < max_cut) { ++data_context.cut_thread_count; pthread_mutex_unlock(&(data_context.mutex)); /* cut list */ int mid = list->size / 2; llist_t *_list = list_new(); _list->head = list_nth(list, mid); _list->size = list->size - mid; list_nth(list, mid - 1)->next = NULL;//cut list->size = mid; /* create new task: left */ task_t *_task = (task_t *) malloc(sizeof(task_t)); _task->func = cut_func; _task->arg = _list; tqueue_push(pool->queue, _task); /* create new task: right */ _task = (task_t *) malloc(sizeof(task_t)); _task->func = cut_func; _task->arg = list; tqueue_push(pool->queue, _task); } else { pthread_mutex_unlock(&(data_context.mutex)); merge(merge_sort(list)); } } ``` - 在我們的認知中,mergesort的示意圖應該如下 - list->size > 1 的情況,如左圖 - cut_count < max_count ,其中max_count假設為3(4條thread),如右圖 ```graphviz digraph mergesort { node [shape = record,height=.1]; node0 [label = "1|2|3|4|5|6"]; node1 [label = "1|2|3"]; node2 [label = "4|5|6"]; node3 [label = "1|2"]; node4 [label = "3"]; node5 [label = "4|5"]; node6 [label = "6"]; node7 [label = "1"]; node8 [label = "2"]; node9 [label = "4"]; node10 [label = "5"]; node0 -> node1; node0 -> node2; node1 -> node3; node1 -> node4; node2 -> node5; node2 -> node6; node3 -> node7; node3 -> node8; node5 -> node9; node5 -> node10; node11 [label = "1|2|3|4|5|6"]; node12 [label = "1|2|3"]; node13 [label = "4|5|6"]; node14 [label = "1|2"]; node15 [label = "3"]; node16 [label = "4|5"]; node17 [label = "6"]; node11 -> node12; node11 -> node13; node12 -> node14; node12 -> node15; node13 -> node16; node13 -> node17; } ``` - 這次的mergesort-concurrency是用thread pool實做。cut_func會不斷新增cut_func到task_queue,給work thread去執行。cut_func停止cut的其一判斷條件為cut_count,cut_count是一個會被多個執行cut_func更動的共享全域變數。所以可能會發生cut_func分到list長度很大,但卻不會繼續切半list,就因為cut_count被其他cut_func更新到等於max_count。所以同樣max_count為3的情況下,list可能會被分成圖下情況 ```graphviz digraph mergesort { node [shape = record,height=.1]; node0 [label = "1|2|3|4|5|6"]; node1 [label = "1|2|3"]; node2 [label = "4|5|6"]; node3 [label = "1|2"]; node4 [label = "3"] node5 [label = "1"]; node6 [label = "2"]; node0 -> node1; node0 -> node2; node1 -> node3; node1 -> node4; node3 -> node5; node3 -> node6; } ``` #### 驗證 - 在cut_func呼叫merge(merge_list(list))前,印出list長度 ```c=133 pthread_mutex_unlock(&(data_context.mutex)); + printf("%d\n", list->size) merge(merge_sort(list)); ``` - 以 128 threads為例,擷取一小段 - 比較好的分佈情況 ``` 2734 1367 2734 1367 1367 1366 1367 2734 1367 1367 2733 2734 2733 1367 1367 1366 2733 ``` - 比較壞的分佈情況 ``` 85 10935 10934 86 10934 10935 342 342 342 86 341 171 341 171 170 171 ``` > 這邊應該要用圖表示比較精確,擷取部份只是為突顯分佈不均的情況[name=cheng hung lin] > 有比對過 contention 數量和分佈的關係嗎? [name=fzzzz] > >還沒比較兩者之間的關係,太急著想改善資料分佈的情況,直接實做相對均勻的分佈,並測試原本和新的兩者之間執行時間的分佈情況[name=cheng hung lin] #### 改善 [code 連結](https://github.com/FZzzz/mergesort-concurrent/blob/data_scala_test/main.c#L109-L145) - cut 策略示意圖,在4條thread的情況,也就是max_count為3 ```graphviz digraph mergesort { node [shape = record,height=.1]; node0 [label = "1|2|3|4|5|6"]; node1 [label = "1|2"]; node2 [label = "3|4|5|6"]; node3 [label = "1"]; node4 [label = "2"]; node5 [label = "3"]; node6 [label = "4|5|6"]; node0 -> node1; node0 -> node2; node1 -> node3; node1 -> node4; node2 -> node5; node2 -> node6; } ``` points ![](https://i.imgur.com/63pc45o.png) linespoints ![](https://i.imgur.com/B6I2AB2.png) > 跟fzzzz討論,認為要將資料分佈的變異數(或標準差)計算出來,因為圖表只能呈現大概的結果,但還是要靠數學量化。[name=cheng hung lin] ## mutrace 參數說明 - `Locked`: how often the mutex was locked during the entire runtime - `Changed`: how often the owning thread of the mutex changed - `Cont.`: how often the lock was already taken when we tried to take it and we had to wait - `tot.Time[ms]`: lock的總時間 - `avg.Time[ms]`: 平均每個lock的時間 - `max.Time[ms]`: lock最久的時間 ## References * 恐龍本 * [Conditional Variable](http://pages.cs.wisc.edu/~remzi/OSTEP/threads-cv.pdf) * [Covariance](http://belleaya.pixnet.net/blog/post/30950579-%5B%E6%95%99%E5%AD%B8%5D-%5B%E7%B5%B1%E8%A8%88%5D-pearson-%E7%9B%B8%E9%97%9C)