# 2016q3 Homework3 (mergesort-concurrent) contributed by <`mingnus`> ###### tags: `ncku` `sysprog` `mergesort` ## Code Review - [ ] threadpool.c * queue的操作應該是push to tail, pop from head, 但是threadpool.c寫反了 >> 很敏銳的觀察,很好!請一併參照 GLib 的 [Asynchronous Queues](https://developer.gnome.org/glib/stable/glib-Asynchronous-Queues.html),裡頭的命名和 programming model 很值得學習 [name=jserv] >> 簡單比較[list實作](https://hackmd.io/s/rkkg8vJJe), 感覺kernel這種允許自訂資料結構的做法比較有彈性 [name=mingnus] * tqueue_push()和tqueue_pop()實作看起來有點複雜, 還要判斷head及tail是不是NULL。可以試看看改用Linux kernel的list實作來簡化code。用circular queue也可省掉tqueue_t的tail pointer。 >> non-circular list也不利於加入節點到list結尾, 例如merge_list()就必須維護一個指向`_list`結尾的指標`current`。不過話說回來merge_list()或許不用另外宣告`_list`, 只要把list b併到list a就好 [name=mingnus] >> kernel list_sort.c的做法滿特別, 它是把原本的list打散成數個null-terminated sorted sublist, 再把它們合併 [name=mingnus] * struct _task的成員命名不好, 連結前後元素的pointer應為prev & next。寫成prev & last容易讓人誤解 * 資料結構封裝不佳: 目前在threadpool.c之外仍可存取tpool_t::queue, 但threadpool的工作很單純,應該沒必要讓客戶端指定worker thread的function。參考Linux kernel workqueue實作: * [《Linux Device Drivers》 3/e, ch7.6](https://static.lwn.net/images/pdf/LDD3/ch07.pdf) * [Concurrency Managed Workqueue](https://www.kernel.org/doc/Documentation/workqueue.txt), Linux kernel documentation * [Concurrency-managed workqueues](http://lwn.net/Articles/355700/), LWN - [ ] main.c * main(): max_cut只計算一次, 應該沒必要為了省略branch而寫成這樣 ```C max_cut = thread_count * (thread_count <= data_count) + data_count * (thread_count > data_count) - 1; ``` 改用一般寫法也比較好理解 ```C max_cut = MIN(thread_count - 1, data_count - 1); ``` * cut_func()是用cut_count決定是否要切割傳入的sublist, 在多執行緒環境下可能導致sublists長度不均 * merge()以FIFO方式合併sublists, 先排完的sublist先合併 (排序完成的sublist儲存於全域變數tmp_list, 等待另一個sublist排序完成後合併), 這樣在多執行緒時無法預測哪些sublist會互相合併, 長度不同的sublist也可能互相合併。為何不在merge()的參數指定要合併的sublists? >> 還沒想到確實做法, sibling list的資訊可能要儲存在list head [name=mingnus] >> 你需要重新回顧資料結構和演算法的設計,可參考 [Skip list](https://en.wikipedia.org/wiki/Skip_list) [name=jserv] * cut_func()在分割list時重覆呼叫list_nth(), 實際上可用list->head->next取代 ```C _list->head = list_nth(list, mid); _list->size = list->size - mid; list_nth(list, mid - 1)->next = NULL; ``` * mutex設計不良。程式包含分割及合併兩種task, 當task開始執行時(`cut_func()`或`merge()`)為了檢查全域變數, 必須持有全域mutex `data_context::mutex`, 這樣容易造成lock contention。但這兩種task可以不必共用mutex: * 分割task: 以sublist長度決定是否要分割, 不必檢查全域變數`cut_thread_count` * 合併task: 以現行的FIFO合併方式, 可能免不了要用一塊shared memory來儲存已排序完成的sublists。存取這塊shared memory還是要握mutex。 >> 非常好!透過 `mutrace`,你可以發現,事實上有些 lock 是根本不需要存在,就像之前 thread pool 的設計中,善用 ring buffer 就可以省去部分 lock 的成本 [name=jserv] * threadpool worker thread function `task_run()` 以polling方式詢問taskqueue, 極易造成lock contention。傳統做法是採用conditional variable避免polling。 * task function命名不佳, 不易看出程式脈絡 * cut_func()從名稱上來看是負責分割list, 但它卻會呼叫merge_sort()及merge() * merge_sort()的地位可提升為task function。merge_sort()不該由cut_func()呼叫, 改由cut_func()直接將不需再分割的sublist傳給merge_sort() * 按照原作邏輯, 我認為寫成下面這樣, 將call graph扁平化比較好理解。雖merge_sort()多了一道merge() task的轉發手續, 不過cut_func()的call graph也得以簡化 ```C void cut_func(void *data) { llist_t *list = (llist_t *) data; int cut_count = data_context.cut_thread_count; if (list->size > 1 && cut_count < max_cut - 1) { ++data_context.cut_thread_count; ... // create new cut_func() tasks for the left & right sublist } else if (cut_count == max_cut - 1) { // create new merge_sort() tasks for the left & right sublist } } void merge_sort(void *data) { llist_t *list = (llist_t *) data; // do the plain mergesort on list // create new merge() task for the sorted list } void merge(void *data) { // like the current merge() does... } ``` * gprof分析 * list_new()用法不當: merge_sort()和merge_list()都會呼叫list_new(), 造成大量malloc() * TODO * lock-free threadpool ## Refactoring * list實作以kernel list.h取代 * Merge sort參考kernel list_sort.c * merge(): * merge_and_restore_back_links(): * 重點: 合併一個節點時, 只需更新其與前一個節點的連結(兩個指標),不需更新與下一個節點的連結(因為下次合併會再更新) ## Lock-free algorithm ## References * Some articles about merge sort * [Linux kernel list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) * https://www.khanacademy.org/computing/computer-science/algorithms/merge-sort/a/analysis-of-merge-sort * http://www.geeksforgeeks.org/merge-sort-for-linked-list/ * Some articles about parallel merge sort * http://zobayer.blogspot.com/2010/09/threaded-merge-sort.html * 很陽春的實作, 未使用threadpool: 每個sublist合併動作都是一個task * 對於長度N的list, 在分割步驟可能會產生N個threads (worst case); 如果系統不允許產生這麼多個thread, 就會產生deadlock * http://www.drdobbs.com/parallel/parallel-in-place-merge-sort/240169094 * 使用Intel TBB, 概念和zobayer的實作很像 * https://blogs.msdn.microsoft.com/pfxteam/2011/06/06/parallel-merge-sort-using-barrier/ * * mergesort.cpp in [threadpool lib](http://threadpool.sourceforge.net) * 不會合併不同長度的list: 所有長度2^n的sublists都合併完成後, 才會開始合併長度2^(n+1)的sublists * 參考共筆 * [TempoJiJi](https://hackmd.io/s/B1-Kytv0) * 簡化list實作, 不記錄list size * 取得list中間元素的方法 [Floyd's cycle-finding algorithm](https://en.wikipedia.org/wiki/Cycle_detection#Tortoise_and_hare) * [andy19950](https://hackmd.io/s/rJ6m3IvC) * multiple workqueue: 每個thread都有自己的workqueue。round-robin將tasks平均分配給每個workqueue * jkrvivian * [Makefile for auto testing](https://github.com/jkrvivian/compute-pi/blob/master/Makefile) * [shelly4132](https://hackmd.io/s/HkX7l7YA) * 合併merge_sort()和cut_func() (把merge_sort()刪掉,留下cut_func()就好) * 副作用: 產生過多的merge tasks? 就算合併兩個長度為1的sublist, 也需產生task * [LanKuDot](https://hackmd.io/s/S1ezGhIA) * scability評估方法[[1]](http://stackoverflow.com/questions/9420014/what-does-it-mean-scalability) * [0140454](https://hackmd.io/s/rkx7_9DR) * 同shelly4132的做法, 合併cut_func()及merge_sort() * [王紹華](https://hackmd.io/s/r1puDF9R) * pipe usage? * [ierosodi](https://hackmd.io/s/HyXCDd_R) * multiple workqueue * lock-free threadpool * [FZzzz](https://hackmd.io/s/SJfa8wFR) * 字串版實驗 * [SwimGlasses](https://hackmd.io/s/r1Y9r_OR) * 實驗 * [vic85821](https://hackmd.io/s/rkyY8VjR) * 改寫thread pool * [CheHsuan](https://hackmd.io/s/ry24aKiA) * 解釋為何執行時間會跳動 * [ruby0109 2016q1筆記](https://embedded2016.hackpad.com/concurrent-merge-sort-p3rF7g0t09S) * bottom-up & top-down