# 2016q3 Homework3 (mergesort-concurrent) contributed by <`TempoJiJi`> ## 預期目標 * 作爲concurrency的展示案例 * 學習 POSIX Thread Programming * 為日後效能分析和 scalability 研究建構基礎建設 * 學習程式品質分析和相關的開發工具 ## 開發環境 * Ubuntu 16.04 LTS * L1d cache: 32K * L1i cache: 32K * L2 cache: 256K * L3 cache: 3072K * Architecture: x86_64 * CPU op-mode(s): 32-bit, 64-bit * Byte Order: Little Endian * CPU(s): 4 * Model name: Intel(R) Core(TM) i5-4200U CPU @ 1.60GHz --- 先來測試程式效能跟檢查結果: ```shell= $ (for i in {1..8}; do echo $RANDOM; done) | ./sort 4 8 ``` ```shell= input unsorted data line-by-line [11562] [17074] [20028] [20439] [20600] [21083] [29465] [32236] ``` --- ## 設計自動測試實驗 大致上閱讀程式碼後,先來設計自動測試的實驗,方便日後修改程式碼時能夠做對比: 先準備產生亂數的程式`intput_generator.c` 接着修改main.c,將`FIX ME`的部分都改成檔案輸入出 最後在Makefile中加入Bash腳本: ```shell bench: for i in `seq 100000 10000 500000`; do \ ./input_generator $$i; \ for j in 1 2 4 8 16 32 64; do \ ./sort $$j $$i; \ done \ done plot: bench gnuplot runtime.gp ``` 執行 ```shell $ make plot ``` 執行結果: ![](https://i.imgur.com/FQ7A1lU.png) 這個結果有點出乎意料,執行緒越多,時間抖動的越厲害,而且sorting的時間比一個執行緒還來的慢 --- ## 閱讀程式碼及Refactoring 首先爲了確保每次更改後,sorting後的結果是正確的,我多加了一個CHECK的機制,每次只需利用`$ make check`就能確認結果有沒有正確: ```shell= check: input_generator for i in `seq 1 1 100`; do\ ./input_generator $$i; \ ./sort 4 $$i; \ sort -n input > sorted; \ diff output sorted; \ done ``` --- 參考老師的解說:[「你所不知道的 C 語言」:物件導向篇 ](https://www.youtube.com/watch?v=IG6q8WX0sK4),首先在`list_add()`中,它並沒有回傳任何的address(list_t),加上傳入的參數只是副本,因此這裏需要改成回傳一個成功list_add的一個address,直接看code吧(這裏也可以使用pointer to pointer,避免傳入的只是副本),這部分不太清楚需不需要避免重復的值,我就直接連重復的值也加進去了: ```c= list_t *list_add(list_t *e, val_t val) { e->next = malloc(sizeof(list_t)); e = e->next; e->data = val; e->next = NULL; return e; } ``` 還有在`tpool_init`時,也需要避免這個問題 接着我把node_t這個結構給刪掉,改成只有一個結構,因爲這樣的改動,所以`list_new()`已經不需要了,所有跟size有關的部分都需要更動: ```c= typedef struct _LIST_T { val_t data; struct _LIST_T *next; } list_t; ``` 再來size這個變數已經沒有了,加上`list_nth`這個function的名字有點怪怪的,所以我把它改名成`getMiddle`也改變了裏面的行爲,就只是單純找出list的中間位置,不需要用到size: ```c= list_t *getMiddle(list_t *list) { if(!list) return list; list_t *slow = list; list_t *fast = list; while(fast->next && fast->next->next){ slow = slow->next; fast = fast->next->next; } return slow; } ``` > Wow,我沒有想過可以這樣找中間的 element XD [name=Kun-Yi Li] 再來是修改`merge_sort`跟`merge_list`的部分,我把它們都寫的比較簡單了一點,這個應該會比之前的容易理解很多: ```c= list_t *merge_list(list_t *a, list_t *b) { list_t *head = malloc(sizeof(list_t)); list_t *cur = head; while (a && b) { if(a->data <= b->data){ cur->next = a; a = a->next; } else if(a->data > b->data){ cur->next = b; b = b->next; } cur = cur->next; } cur->next = (!a) ? b : a; return head->next; } list_t *merge_sort(list_t *list) { if (!list || !list->next) return list; list_t *mid = getMiddle(list); list_t *half = mid->next; mid->next = NULL; return merge_list(merge_sort(list), merge_sort(half)); } ``` --- 排序的部分大致上都處理完畢後,來看一看關於lock contention的問題,觀察`cut_func`,其實這個這個部分並不需要lock,因爲各個thread都在處理各自的list,並不會影響到對方,所以我把這個部分的mutex_lock給拿掉了,這樣就減少了很多lock contention的問題: ```c= void cut_func(void *data) { list_t *list = (list_t *) data; if (list && list->next && cut_count <= max_cut) { cut_count++; /* cut list */ list_t *mid = getMiddle(list); list_t *half = mid->next; mid->next = NULL; .... .... ``` 接着`task_run`的部分我將它簡化了一點,不是利用`NULL`來終止,而是設立一個變數`shutdown`: ```c= static void *task_run(void *data) { (void) data; while (!shutdown) { task_t *task = tqueue_pop(pool->queue); if (task) task->func(task->arg); } pthread_exit(NULL); } ``` 最後在`task_run`這個部分其實有點疑問,如果沒有block着會不會一直佔着CPU的資源,所以我就想說沒有task時就block着,空出CPU的資源,有task時才進行,不知道這樣的效率會不會比較好,原本想說要做出來比較看看,可是一直遇到bug...(背景知識還不夠熟悉) 最後查看執行結果: ![](https://i.imgur.com/QgEHUAW.png) 這裏雖然簡化了程式碼,但結果並沒有好很多,還有部分需要改進,還需要更加了解關於Thread pool跟[synchronization object](https://docs.oracle.com/cd/E19683-01/806-6867/6jfpgdcnd/index.html)的背景知識才行... >> 提示: 試著透過 [phonebook-concurrent](https://hackmd.io/s/rJsgh0na) 提到的 SuperMalloc,比照執行結果 [name=jserv] --- ## SuperMalloc 經過老師提點後,就來嘗試看看,在[phonebook-concurrent](https://hackmd.io/s/rJsgh0na)中有提到的SuperMalloc,從[SuperMalloc](https://github.com/sysprog21/SuperMalloc)上下載程式碼安裝後,就可以開始使用,只要在`SuperMalloc/release`這個路徑底下輸入指令,就能使用: ```shell= $ LD_PRELOAD=libsupermalloc_pthread.so 程式路徑 ``` 利用SuperMalloc的執行結果: ![](https://i.imgur.com/XsBk3WA.png) 這裏可以從橫軸中很明顯的看到,最大值只到1.6sec,跟之前的比起來效能快了很多 Original vs SuperMalloc: ![](https://i.imgur.com/oQQgUsD.png) 由於還在啃論文中,所以還需要一點時間來理解關於SuperMalloc的原理跟行爲 參考資料: [SuperMalloc: A Super Fast Multithreaded Malloc for 64-bit Machines](http://supertech.csail.mit.edu/papers/Kuszmaul15.pdf) ###### tags: `TempoJiJi` `mergesort-concurrent` `sysprog21`