--- tags: ncku-course --- # 2016q3 Homework3 (mergesort-concurrent) contributed by <`0140454`> ## 閱讀程式碼 ### list.h 首先看看 linked list 的結構 ```clike= typedef struct llist { node_t *head; uint32_t size; } llist_t; ``` 原本的程式在建立 linked list 的時候就會順便紀錄長度,所以在分割 list 的時候只需要花 $O(N)$ 的時間就好,不需要重新計算長度。 實際的資料則是儲存於 `node_t`,並利用 `typedef intptr_t val_t` 來定義自己的 type。 如此一來,若要更換資料類型,只需要改變 typedef 語句就好。 ```clike= typedef struct node { val_t data; struct node *next; } node_t; ``` ### list.c 這一部份主要是處理 linked list 的各種操作。 在這邊比較有爭議的地方應該就是 `list_nth()` 這個函數,從字面上是表示取得第 n 個 node,但在實作上所返回的卻是 index 為 n 的 node。這一部份會在 code refactoring 做修改。 ### threadpool.[ch] 這邊的 thread pool 是採用單一 queue 的架構。 先看看 queue 的部份,利用 mutex 來確保不會有 race condition 的發生。而 condition variable 在觀看的時候發現只有被初始化而已,沒有其他地方使用到。 ```clike= typedef struct { task_t *head, *tail; pthread_mutex_t mutex; pthread_cond_t cond; uint32_t size; } tqueue_t; ``` 再來是儲存工作資訊的結構,所採用的 doubly linked list。紀錄前一個 task 的用意在於,當從 queue 中拿出工作後,可以在 $O(1)$ 的時間將 queue 的 tail 指向應有的地方。 ```clike= typedef struct _task { void (*func)(void *); void *arg; struct _task *next, *last; } task_t; ``` 在 queue 的部份,原本程式碼的設計是從 head 插入,從 tail 拿出來。雖然符合 FIFO 的定義,但對於常規來說,有點不符合直覺。因此這一部份是可以改善的。 ### main.c 看到 `cut_func()` 時發現有用`max_cut` 來限制切割次數,但是不太了解實際的功用。因為在函數中已經有透過 `if (list->size > 1)` 來判斷了。 這一部份與[賴紹芊同學](https://hackmd.io/MwQwRgpiAcAMBmBaA7MALMxaBsAmArItLsNIgJwjACMI2yu02wEQA===?view)討論後,有詢問過作者。發現移除掉 `cut_count < max_cut` 之後,結果並沒有異常。 >> 太好了,你可以大幅縮減程式碼 [name=jserv] 另外,merge sort 的東西都混在 main.c 中,未來可以把他提取出來,成為一個檔案,這樣 main.c 比較簡潔。 ## Code Refactoring ### list.c * list_add 該函數所 return 的東西永遠是 0。我們可以改成 return list head,在使用上能夠更加彈性。 ```clike= llist_t *list_add(llist_t *list, val_t val) { node_t *e = node_new(val, NULL); e->next = list->head; list->head = e; list->size++; return list; } ``` * list_nth 如同閱讀程式碼那邊所說的,應該把函數名稱改為 `list_get()` 較為妥當。 除此之外,index 不應該大於 size - 1,因此在函數一開始的邊界檢查,應改為 `if (idx >= list->size)` 才對。 ```clike= node_t *list_get(llist_t *list, uint32_t idx) { if (idx >= list->size) return NULL; node_t *head = list->head; while (idx--) head = head->next; return head; } ``` ### threadpool.[ch] 把 `tqueue_t` 中的 cond 移除,因為在 threadpool.c 中只有初始化他卻沒有使用他。 ```clike= typedef struct { task_t *head, *tail; pthread_mutex_t mutex; uint32_t size; } tqueue_t; ``` 再來是剛剛上面提過的,queue 的 push/pop 方向有點不符合直覺。 所以這邊把他改成從頭 pop,從尾巴 push。 但改完發現,其實 `task_t` 間並不需要利用 doubly linked list 來串,因為誰是前一個並不重要。所以 `task_t` 變修改成以下的樣子。 ```clike= typedef struct _task { void (*func)(void *); void *arg; struct _task *next; } task_t; ``` 另外,在 `tqueue_free()` 中是使用 free() 來釋放 `task_t` 記憶體。可是,這樣會導致 task argument 沒有被釋放到,所以應該要改用 `task_free()` 才對。 值得注意的是,在 main.c 的 `merge()` 中,有一段 code 如下 ```clike= ... _task->func = NULL; ... ``` 因為這邊沒有將 `_task->arg` 設成 NULL,因此會造成 free 有錯誤產生,只要將他初始化就能解決問題。 ### main.c 上禮拜 code review 時老師提到 main.c 中不應該過於龐大、複雜。在原始的程式碼中,main.c 總共有 183 行,如果把 merge sort 相關的函數分離,應該可以減少不少行。 首先,先針對上面說的,`max_cut` 的切割限制似乎沒有太重要的地方,所以先把相關的部份移除。mutex 之所以還需要留著是因為有其他的 static global variable 需要受到保護。 ```clike= struct { pthread_mutex_t mutex; } data_context; ``` 再來是,因為因為 `cut_func()` 已經將切割 list 的動作都做好了,所以 `merge_sort()` 中不需要再進行切割,因此這一部份的 code 是可以移除的。 最後,把 `merge_list()` 抽出,放置 merge_sort.c 中,讓 main.c 裏面只留必要的東西。除此之外,也把 `merge()` 改名成 `merge_threads_list()`,因為他主要是在合併 thread 間已經 sort 完成的 list。 ## 修改程式 ### 讓程式能夠接受 phonebook 的 dictionary 首先,將 list.h 中的 `val_t` 改定義為 char array。 ```clike= typedef char val_t[16]; ``` 並將 input/output 部份改用 `%s` 來操作。 最後修改 merge_sort.c 中的 `merge_list()`,讓他使用 `memcpy()` 來比較。 ### 設計自動化測試 在 Makefile 中加入以下規則,利用系統內建的工具來隨機產生資料以及比較結果。 ``` THREAD_NUM ?= $(shell nproc) check: all sort -R dictionary/words.txt | ./sort $(THREAD_NUM) $(shell wc -l dictionary/words.txt) > sorted.txt diff -u dictionary/words.txt sorted.txt && echo "Verified!" || echo "Failed!" ``` ### 改為 lock-free 版本 **原始版本效能** 首先,先來看看 merge sort 所花的時間以及 mutex 的使用情況。 ```shell= execution time: 0.511178 sec mutrace: 3 mutexes used. Mutex #0 (0x0x2d43e20) first referenced by: /usr/lib/mutrace/libmutrace.so(pthread_mutex_init+0xf2) [0x7f762e57b4b2] ./sort(tqueue_init+0x38) [0x40173a] ./sort(tpool_init+0x6a) [0x401959] ./sort(main+0x119) [0x4014cf] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f762dfb2830] Mutex #2 (0x0x603120) first referenced by: /usr/lib/mutrace/libmutrace.so(pthread_mutex_init+0xf2) [0x7f762e57b4b2] ./sort(main+0xdf) [0x401495] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f762dfb2830] Mutex #1 (0x0x7f762bb81860) first referenced by: /usr/lib/mutrace/libmutrace.so(pthread_mutex_lock+0x49) [0x7f762e57b6b9] /lib/x86_64-linux-gnu/libgcc_s.so.1(_Unwind_Find_FDE+0x2c) [0x7f762b97dfec] [(nil)] mutrace: Showing 3 most contended mutexes: Mutex # Locked Changed Cont. tot.Time[ms] avg.Time[ms] max.Time[ms] Flags 0 2238787 1724928 1040095 298.548 0.000 0.049 Mx.--. 2 699798 567352 117699 80.506 0.000 0.039 M-.--. 1 20 13 3 0.003 0.000 0.001 M-.--. |||||| /||||| Object: M = Mutex, W = RWLock /|||| State: x = dead, ! = inconsistent /||| Use: R = used in realtime thread /|| Mutex Type: r = RECURSIVE, e = ERRRORCHECK, a = ADAPTIVE /| Mutex Protocol: i = INHERIT, p = PROTECT / RWLock Kind: r = PREFER_READER, w = PREFER_WRITER, W = PREFER_WRITER_NONREC ``` 從 mutrace 的結果可以看出,在維護 thread pool 的 queue 時,總共用了不少的時間。接著,先針對 thread pool 進行改善。 **lock-free thread pool**