Lin Cheng-Chieh
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Versions and GitHub Sync Note Insights Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       owned this note    owned this note      
    Published Linked with GitHub
    Subscribed
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    Subscribe
    --- tags: linux2022 --- # 2022q1 Homework1 (lab0) contributed by < `blueskyson` > ## 開發環境 ```shell $ gcc --version gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0 $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian Address sizes: 39 bits physical, 48 bits virtual CPU(s): 12 On-line CPU(s) list: 0-11 Thread(s) per core: 2 Core(s) per socket: 6 Socket(s): 1 NUMA node(s): 1 Vendor ID: GenuineIntel CPU family: 6 Model: 165 Model name: Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz Stepping: 2 CPU MHz: 2600.000 CPU max MHz: 5000.0000 CPU min MHz: 800.0000 BogoMIPS: 5199.98 Virtualization: VT-x L1d cache: 192 KiB L1i cache: 192 KiB L2 cache: 1.5 MiB L3 cache: 12 MiB NUMA node0 CPU(s): 0-11 ``` ## lab0-c 開發過程 參考[你所不知道的 C 語言:linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list#Linked-list-%E5%9C%A8-Linux-%E6%A0%B8%E5%BF%83%E5%8E%9F%E5%A7%8B%E7%A8%8B%E5%BC%8F%E7%A2%BC)實作 ![](https://hackmd.io/_uploads/ByHFSCRit.png) 以下開發過程以 Head 表示佇列的頭,以 Node 表示 `element_t` 結構體。 ### q_new 首先 `malloc` 一個 `struct list_head *head` 作為佇列的 Head,並檢查 `head` 是否為 `NULL`。接下來我把 `head->prev` 和 `head->next` 初始化為 `head`,`INIT_LIST_HEAD` 函式已經實作了前述初始化。 ```c struct list_head *q_new() { struct list_head *head = malloc(sizeof(struct list_head)); if (!head) return NULL; INIT_LIST_HEAD(head); return head; } ``` 參考 [kdnvt](https://hackmd.io/@5ux520T8Sd-OoYfdxZR55Q/SyzDKJ9y9),若 malloc `head` 失敗,`head` 本身就會指向 `NULL`。所以程式碼還能改得更短: ```c struct list_head *q_new() { struct list_head *head = malloc(sizeof(struct list_head)); if (head) INIT_LIST_HEAD(head); return head; } ``` ### q_free 首先確認傳入的 `struct list_head *l` 是否為 `NULL`。接下來使用 `list_for_each_entry_safe` 走訪佇列中所有 Node,使用 `list_del` 將 `node` 從佇列移除並且 `free` 掉 `node->value` 和 `node` 本身。最後 `free` 掉 `l`,即佇列的 Head。 ```c void q_free(struct list_head *l) { if (!l) return; element_t *node, *tmp; list_for_each_entry_safe (node, tmp, l, list) { list_del(&node->list); free(node->value); free(node); } free(l); } ``` ### q_insert_head 檢查 Head 是否為 `NULL`。`malloc` 一個 `element_t *node` 作為即將插入佇列的 Node,並檢查 `node` 是否為 `NULL`。接下來透過 `strdup` 將字元陣列 `s` 複製到 `node->value`。最後呼叫 `list_add` 將 `node` 插入佇列的開頭。 ```c bool q_insert_head(struct list_head *head, char *s) { if (!head) return false; element_t *node = malloc(sizeof(element_t)); if (!node) return false; node->value = strdup(s); if (!node->value) { free(node); return false; } list_add(&node->list, head); return true; } ``` ### q_insert_tail 檢查 Head 是否為 `NULL`。`malloc` 一個 `element_t *node` 作為即將插入佇列的 Node,並檢查 `node` 是否為 `NULL`。接下來透過 `strdup` 將字元陣列 `s` 複製到 `node->value`。最後呼叫 `list_add_tail` 將 `node` 插入佇列的尾端。 ```c bool q_insert_tail(struct list_head *head, char *s) { if (!head) return false; element_t *node = malloc(sizeof(element_t)); if (!node) return false; node->value = strdup(s); if (!node->value) { free(node); return false; } list_add_tail(&node->list, head); return true; } ``` ### q_remove_head 檢查 Head 是否為 `NULL`、檢查佇列中是否有 Node。透過 `list_entry` 取得佇列的第一個 Node,然後用 `list_del` 移除此 Node。接下來檢查 `sp` 是否為 `NULL` 以及 `bufsize` 是否為 0,使用 `strncpy` 把 Node 的字元陣列複製到 `sp` 中。 改進: - 以 `list_empty(head)` 替換 `head == head->next` - 以 `list_first_entry(head, element_t, list)` 替換 `list_entry(head->next, element_t, list)` ```c if (!head || list_empty(head)) return NULL; element_t *node = list_first_entry(head, element_t, list); list_del(head->next); if (sp && bufsize) { strncpy(sp, node->value, bufsize - 1); sp[bufsize - 1] = '\0'; } return node; } ``` ### q_remove_tail 檢查 Head 是否為 `NULL`、檢查佇列中是否有 Node。透過 `list_entry` 取得佇列的最後一個 Node,然後用 `list_del` 移除此 Node。接下來檢查 `sp` 是否為 `NULL` 以及 `bufsize` 是否為 0,使用 `strncpy` 把 Node 的字元陣列複製到 `sp` 中。 改進: - 以 `list_empty(head)` 替換 `head == head->next` - 以 `list_last_entry(head, element_t, list)` 替換 `list_entry(head->prev, element_t, list)` ```c element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize) { if (!head || list_empty(head)) return NULL; element_t *node = list_last_entry(head, element_t, list); list_del(head->prev); if (sp && bufsize) { strncpy(sp, node->value, bufsize - 1); sp[bufsize - 1] = '\0'; } return node; } ``` ### q_size 檢查 Head 是否為 `NULL`,接下來透過 `list_for_each` 走訪整個佇列以計算 Node 的數量。 ```c int q_size(struct list_head *head) { if (!head) return 0; int len = 0; struct list_head *li; list_for_each (li, head) len++; return len; } ``` ### q_delete_mid 宣告 `struct list_head *slow = head->next, *fast =slow->next`,`slow` 每次會指向 `slow` 的下一個 `list_head`,而 `fast` 會指向 `fast` 的下下個 `list_head`,所以當 `fast` 指向 `head` 時,`slow` 正好在佇列正中間的 Node。然後,透過 `list_entry` 得到 `slow` 所在的 Node 的位址、透過 `list_del` 把 slow 從佇列中移除,最後 `free` 掉這個 Node 的所有資料。 改進: - 以 `list_empty(head)` 替換 `head == head->next` ```c bool q_delete_mid(struct list_head *head) { if (!head || list_empty(head)) return false; // find the middle node struct list_head *slow = head->next, *fast = slow->next; while (fast != head) { fast = fast->next; if (fast != head) { fast = fast->next; slow = slow->next; } } element_t *node = list_entry(slow, element_t, list); list_del(slow); free(node->value); free(node); return true; } ``` ### q_delete_dup 確認佇列的 Head 是否為 `NULL`。在 `list_for_each_entry_safe` 的每次迭代,`prev_value` 會指向前一個未重複的字串,若 `prev_value` 所儲存的字串與 `node->value` 一模一樣,代表字串重複,將當前的 Node 從佇列移除;反之將 `prev_value` 指向 `node->value`。 ```c bool q_delete_dup(struct list_head *head) { if (!head) return false; element_t *node, *tmp; char *prev_value = ""; list_for_each_entry_safe (node, tmp, head, list) { if (strcmp(prev_value, node->value) == 0) { list_del(&node->list); free(node->value); free(node); } else { prev_value = node->value; } } return true; } ``` [#73](https://github.com/sysprog21/lab0-c/pull/73) 修正了評分程式的 bug,我上面的 `q_delete_dup` 不能通過測試,所以我修正了程式碼: ```c bool q_delete_dup(struct list_head *head) { if (!head) return false; element_t *node, *tmp; char *prev_value = ""; bool only_one = true; list_for_each_entry_safe (node, tmp, head, list) { if (strcmp(prev_value, node->value) == 0) { only_one = false; list_del(&node->list); free(node->value); free(node); } else { prev_value = node->value; if (!only_one) { element_t *del = list_last_entry(&node->list, element_t, list); list_del(&del->list); free(del->value); free(del); only_one = true; } } } // If tail nodes are duplicated if (!only_one) { element_t *del = list_last_entry(head, element_t, list); list_del(&del->list); free(del->value); free(del); only_one = true; } return true; } ``` ### q_swap 確認佇列的 Head 是否為 `NULL`。用 `first` 和 `second` 指向一對連續的 `list_head`,操作 `first->prev`、`first`、`second`、`second->next` 的指標來達成 swap,最後將 `first`、`second` 指向下一對連續的 `list_head`。 ```c void q_swap(struct list_head *head) { if (!head) return; struct list_head *first = head->next, *second = first->next; while (first != head && second != head) { first->prev->next = second; second->prev = first->prev; first->prev = second; first->next = second->next; second->next->prev = first; second->next = first; // point to next pair first = first->next; second = first->next; } } ``` 改進: - 參考 [SmallHanley](https://hackmd.io/@SmallHanley/linux2022-lab0) 的作法,用 list.h 的 API 取代所有指標操作。 ```c void q_swap(struct list_head *head) { if (!head) return; struct list_head *first; list_for_each (first, head) { if (first->next == head) { break; } list_del(first); list_add(first, first->next); } } ``` ### q_reverse 首先確認佇列的 Head 是否為 `NULL`。接下來宣告 `struct list_head *prev, *node` 代表 circular linked list 中的任兩個連續的 `list_head`,接下來透過 `do while` 迴圈,把 `prev->prev` 與 `prev->next` 指向的位址對調,然後把 `prev` 和 `node` 指向下兩個連續的 `list_head`,直到所有 `list_head` 的 `prev` 和 `next` 指向的位址都被對調。 ```c void q_reverse(struct list_head *head) { if (!head) return; struct list_head *prev = head, *node = head->next; do { prev->next = prev->prev; prev->prev = node; prev = node; node = node->next; } while (prev != head); } ``` 圖示: **尚未反轉的佇列** ```graphviz digraph ele_list { rankdir=LR; node[shape=record]; e1 [label="<top>Head |{<left>prev|<right>next}", style="bold", pos="0,0!"]; e2 [label="<top>Node 1|{<left>prev|<right>next}", style="bold", pos="0,1!"]; e3 [label="<top>Node 2|{<left>prev|<right>next}", style="bold"]; e4 [label="<top>...|{<left>|<right>}", style="bold"]; e5 [label="<top>Node N|{<left>prev|<right>next}", style="bold"]; e1:left:s -> e5:s; e2:left -> e1:top; e3:left -> e2:top; e4:left -> e3:top; e5:left -> e4:top; e5:right -> e1:s; e1:right -> e2:top; e2:right -> e3:top; e3:right -> e4:top; e4:right -> e5:top; } ``` **Step 0: 反轉 Head** ```graphviz digraph ele_list { rankdir=LR; node[shape=record]; e1 [label="<top>Head |{<left>prev|<right>next}", style="bold"]; e2 [label="<top>Node 1|{<left>prev|<right>next}", style="bold"]; e3 [label="<top>Node 2|{<left>prev|<right>next}", style="bold"]; e4 [label="<top>...|{<left>|<right>}", style="bold"]; e5 [label="<top>Node N|{<left>prev|<right>next}", style="bold"]; e1:left:n -> e2:left:nw [color=red]; e2:left -> e1:top; e3:left -> e2:top; e4:left -> e3:top; e5:left -> e4:top; e5:right -> e1:s; e1:right -> e5 [color=red]; e2:right -> e3:top; e3:right -> e4:top; e4:right -> e5:top; p [label="prev", style=dashed, color=grey]; p -> e1:n [style=dashed, color=grey]; n [label="node", style=dashed, color=grey]; n -> e2:n [style=dashed, color=grey]; } ``` **Step 1: 反轉 Node 1** ```graphviz digraph ele_list { rankdir=LR; node[shape=record]; e1 [label="<top>Head |{<left>prev|<right>next}", style="bold"]; e2 [label="<top>Node 1|{<left>prev|<right>next}", style="bold"]; e3 [label="<top>Node 2|{<left>prev|<right>next}", style="bold"]; e4 [label="<top>...|{<left>|<right>}", style="bold"]; e5 [label="<top>Node N|{<left>prev|<right>next}", style="bold"]; e1:left:n -> e2 [color=blue]; e2:left:s -> e3:left:s [color = red]; e3:left -> e2:top; e4:left -> e3:top; e5:left -> e4:top; e5:right -> e1:s; e1:right:s -> e5:left:s [color=blue]; e2:right:w -> e1:right:e [color=red]; e3:right -> e4:top; e4:right -> e5:top; p [label="prev", style=dashed, color=grey]; p -> e2:n [style=dashed, color=grey]; n [label="node", style=dashed, color=grey]; n -> e3:n [style=dashed, color=grey]; } ``` **Step N: 反轉 Node N** ```graphviz digraph ele_list { rankdir=LR; node[shape=record]; e1 [label="<top>Head |{<left>prev|<right>next}", style="bold"]; e2 [label="<top>Node 1|{<left>prev|<right>next}", style="bold"]; e3 [label="<top>Node 2|{<left>prev|<right>next}", style="bold"]; e4 [label="<top>...|{<left>|<right>}", style="bold"]; e5 [label="<top>Node N|{<left>prev|<right>next}", style="bold"]; e1:left -> e2:left:s [color=blue]; e2:left -> e3:left:s [color=blue]; e3:left -> e4:left:s [color=blue]; e4:left -> e5:left:s [color=blue]; e5:left -> e1:top:e [color=red]; e5:right:s -> e4:right:e [color=red]; e1:right -> e5:top:w [color=blue]; e2:right:w -> e1:right:e [color=blue]; e3:right:w -> e2:right:e [color=blue]; e4:right:w -> e3:right:e [color=blue]; p [label="prev", style=dashed, color=grey]; p -> e5:n [style=dashed, color=grey]; n [label="node", style=dashed, color=grey]; n -> e1:n [style=dashed, color=grey]; } ``` ### q_sort 實作 merge sort。想法:在排序過程中把佇列的 Head 斷開、把所有 Node 當作 singly-linked list,當排序完成,再把佇列恢復成 circular doubly-linked list。 :::warning 注意連字號擺放位置: singly-linked, doubly-linked :notes: jserv ::: 首先讓最後一個 Node 的 `next` 指向 `NULL`,將 `&(head->next)` 傳入遞迴函式 `sort_recur`。在 `sort_recur` 中,以傳入的 list 少於兩個 Node 作為中止條件,若此段 list 的長度大於等於兩個 Node,則把它平分成 `left`、`right` 左右兩段,呼叫 `sort_recur(&left)`, `sort_recur(&right)` 進入下一層遞迴。當 `sort_recur` 回傳後,用 `strcmp()` 比較字串長度及大小,把 `left` 及 `right` 的所有 Node 由小到大排序。 當 `sort_recur` 的遞迴結束,佇列已經排序完成,但是所有 `list_head` 的 `prev` 都亂指一通,所以我再用一個 `while` 迴圈來把 `prev` 指向前一個 Node 的,然後再讓最後一個 Node 的 `next` 指向 Head,至此佇列便恢復成 circular doubly linked list。 ```c void sort_recur(struct list_head **phead) { // terminal condition if (*phead == NULL || (*phead)->next == NULL) return; // find the middle node struct list_head *slow = *phead, *fast = slow->next; while (fast != NULL) { fast = fast->next; if (fast != NULL) { fast = fast->next; slow = slow->next; } } // split list struct list_head *left = *phead, *right = slow->next; slow->next = NULL; sort_recur(&left); sort_recur(&right); // merge splited lists struct list_head dummy_head; struct list_head *cursor = &dummy_head; while (true) { if (left == NULL) { cursor->next = right; break; } else if (right == NULL) { cursor->next = left; break; } char *leftval = list_entry(left, element_t, list)->value; char *rightval = list_entry(right, element_t, list)->value; if (strcmp(leftval, rightval) <= 0) { cursor->next = left; left = left->next; } else { cursor->next = right; right = right->next; } cursor = cursor->next; } *phead = dummy_head.next; } void q_sort(struct list_head *head) { if (!head || head == head->next) return; // treat the queue as a singly linked list // let the last node's next point to NULL head->prev->next = NULL; sort_recur(&(head->next)); // turn the list back to doubly circular linked list struct list_head *prev = head, *node = head->next; while (node) { node->prev = prev; prev = node; node = node->next; } // link head and the last node prev->next = head; head->prev = prev; } ``` `sort_recur` 應用 pointer-to-pointer 與 dummy_head 使程式碼精簡。 圖示如下: **尚未排序的佇列** ```graphviz digraph ele_list { rankdir=LR; node[shape=record]; e1 [label="<top>Head |{<left>prev|<right>next}", style="bold", pos="0,0!"]; e2 [label="<top>dolphin|{<left>prev|<right>next}", style="bold", pos="0,1!"]; e3 [label="<top>bear|{<left>prev|<right>next}", style="bold"]; e4 [label="<top>...|{<left>|<right>}", style="bold"]; e5 [label="<top>gerbil|{<left>prev|<right>next}", style="bold"]; e1:left:s -> e5:s; e2:left -> e1:top; e3:left -> e2:top; e4:left -> e3:top; e5:left -> e4:top; e5:right -> e1:s; e1:right -> e2:top; e2:right -> e3:top; e3:right -> e4:top; e4:right -> e5:top; } ``` **前處理:** 把佇列視為 singly linked list,讓最後一個 Node 的 `next` 指向 `NULL`。 ```graphviz digraph ele_list { rankdir=LR; node[shape=record]; e1 [label="<top>Head |{<left>prev|<right>next}", style="bold", pos="0,0!", color=grey]; e2 [label="<top>dolphin|{<left>prev|<right>next}", style="bold", pos="0,1!"]; e3 [label="<top>bear|{<left>prev|<right>next}", style="bold"]; e4 [label="<top>...|{<left>|<right>}", style="bold"]; e5 [label="<top>gerbil|{<left>prev|<right>next}", style="bold"]; NULL [label=NULL, style="bold", color=red, fontcolor=red]; e1:left:s -> e5:s [color=grey]; e2:left -> e1:top [color=grey]; e3:left -> e2:top [color=grey]; e4:left -> e3:top [color=grey]; e5:left -> e4:top [color=grey]; e5:right -> NULL [color=red]; e1:right -> e2:top [color=grey]; e2:right -> e3:top; e3:right -> e4:top; e4:right -> e5:top; } ``` **Merge sort:** 示意圖摘自 [你所不知道的 C 語言:linked list 和非連續記憶體](https://hackmd.io/YA7RMqd6RE2UL8OmmjU9PQ?view#Merge-Sort-%E7%9A%84%E5%AF%A6%E4%BD%9C) ```graphviz digraph G { compound=true node[shape=box, height=.1];sorted_1 sorted_2 sorted_3 sorted_4 sorted_5 sorted_6 sorted_7 sorted_8; node[shape=record, height=.1];input result divide_41 divide_42 divide_21 divide_22 divide_23 divide_24 merge_21 merge_22 merge_23 merge_24 merge_41 merge_42; input[label="<f0>2|<f1>5|<f2>4|<f3>6|<f4>8|<f5>1|<f6>7|<f7>3"] result[label="<f0>1|<f1>2|<f2>3|<f3>4|<f4>5|<f5>6|<f6>7|<f7>8"] divide_41[label="<f1>2|<f2>5|<f3>4|<f4>6"] divide_42[label="<f1>8|<f2>1|<f3>7|<f4>3"] divide_21[label="<f0>2|<f1>5"] divide_22[label="<f0>4|<f1>6"] divide_23[label="<f0>8|<f1>1"] divide_24[label="<f0>7|<f1>3"] sorted_1[label="1"] sorted_2[label="2"] sorted_3[label="3"] sorted_4[label="4"] sorted_5[label="5"] sorted_6[label="6"] sorted_7[label="7"] sorted_8[label="8"] merge_21[label="<f0>2|<f1>5"] merge_22[label="<f0>4|<f1>6"] merge_23[label="<f0>1|<f1>8"] merge_24[label="<f0>3|<f1>7"] merge_41[label="<f1>2|<f2>4|<f3>5|<f4>6"] merge_42[label="<f1>1|<f2>3|<f3>7|<f4>8"] subgraph cluster_0 { style=filled; color="#ef6548"; label="divide"; divide_pad[label="----------------------", style=invis] divide_pad -> divide_23[style=invis] input -> divide_41 input -> divide_42 divide_41 -> divide_21 divide_41 -> divide_22 divide_42 -> divide_23 divide_42 -> divide_24 } divide_21:f0 -> sorted_2 divide_21:f1 -> sorted_5 divide_22 -> sorted_4 divide_22 -> sorted_6 divide_23:f0 -> sorted_8 divide_23:f1 -> sorted_1 divide_24:f0 -> sorted_7 divide_24:f1 -> sorted_3 subgraph cluster_1 { style=filled; color="#a6cee3"; label="sorted lists"; sorted_1; sorted_2; sorted_3; sorted_4; sorted_5; sorted_6; sorted_7; sorted_8; } sorted_2 -> merge_21:f0 sorted_5 -> merge_21:f1 sorted_4 -> merge_22:f0 sorted_6 -> merge_22:f1 sorted_8 -> merge_23:f1[constraint=false] sorted_1 -> merge_23:f0 sorted_7 -> merge_24:f1 sorted_3 -> merge_24:f0 subgraph cluster_2 { style=filled; color="#b2df8a"; label="merge"; merge_pad[label="--------------------", style=invis] rank=same; merge_41; merge_pad; merge_42 rank=same; merge_41; merge_42; merge_pad; merge_22 -> merge_pad[style=invis] merge_23 -> merge_pad[style=invis] merge_pad -> result[style=invis] merge_21 -> merge_41 merge_22 -> merge_41 merge_23 -> merge_42 merge_24 -> merge_42 merge_41 -> result merge_42 -> result } } ``` **完成排序的佇列:** 此時所有 Node 的 `next` 按照順序連接,但是 `prev` 亂指一通。 ```graphviz digraph ele_list { rankdir=LR; node[shape=record]; e1 [label="<top>Head |{<left>prev|<right>next}", style="bold", pos="0,0!"]; e2 [label="<top>bear|{<left>prev|<right>next}", style="bold", pos="0,1!"]; e3 [label="<top>dolphin|{<left>prev|<right>next}", style="bold"]; e4 [label="<top>...|{<left>|<right>}", style="bold"]; e5 [label="<top>vulture|{<left>prev|<right>next}", style="bold"]; NULL [label=NULL, style="bold"]; e1:left:s -> e4:right:s [color=grey]; e2:left:s -> e4:s [color=grey]; e3:left:s -> e4:sw [color=grey]; e5:left:s -> e4:se [color=grey]; e5:right -> NULL; e1:right -> e2; e2:right -> e3:left; e3:right -> e4:left; e4:right -> e5:left; } ``` **讓佇列恢復成 circular doubly linked list** ```graphviz digraph ele_list { rankdir=LR; node[shape=record]; e1 [label="<top>Head |{<left>prev|<right>next}", style="bold", pos="0,0!"]; e2 [label="<top>bear|{<left>prev|<right>next}", style="bold", pos="0,1!"]; e3 [label="<top>dolphin|{<left>prev|<right>next}", style="bold"]; e4 [label="<top>...|{<left>|<right>}", style="bold"]; e5 [label="<top>vulture|{<left>prev|<right>next}", style="bold"]; e1:left:s -> e5:s [color=red]; e2:left -> e1:top [color=red]; e3:left -> e2:top [color=red]; e4:left -> e3:top [color=red]; e5:left -> e4:top [color=red]; e5:right -> e1:s [color=red]; e1:right -> e2:top; e2:right -> e3:top; e3:right -> e4:top; e4:right -> e5:top; } ``` ## 研讀 [list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) **閱讀時間點:** Latest commit [9dbbc3b](https://github.com/torvalds/linux/commit/9dbbc3b9d09d6deba9f3b9e1d5b355032ed46a75) on Jul 8, 2021 `list_sort` 實做為 $2:1$ 的 balanced merge,即,假設現在有兩個長度為 $2^k$ 的 pending sublist (正等著被 merge),如果這兩個 sublist 後面還有 $2^k$ 以上個元素,就馬上 merge 這兩個 sublist 成為 $2^{k+1}$ 長度的 list,如此一來合併後的 list 與剩下的 $2^k$ 個元素數量正好為 $2:1$。 這樣做的好處是,當 cache 容納得下 $3*2^k$ 個元素時,可以避免 cache thrashing。這個實作沒有比 bottom-up merge sort 好,但是可以減少 $0.2*n$ 次比較,所以當 L1 cache 可以容納 $3*2^k$ 個元素時此作法會比較快。 :::info 既然 $2:1$ balanced merge sort 可以減少 $0.2*n$ 次比較,不知道為什麼註解說 bottom-up merge sort 要比 $2:1$ balance merge sort 好,$0.2*n$ 這個數字怎麼來的也不知道。需要計算數學與設計實驗測量 branch、cache miss 數量。 > 快去讀論文! > :notes: jserv ::: 為了找到更詳細的資料,我查看了 list_sort.c 的 commit 紀錄,發現 $2:1$ merge 的實作在 [b5c56e0](https://github.com/torvalds/linux/commit/b5c56e0cdd62979dd538e5363b06be5bdf735a09) 引入,commit 訊息解釋了為何要如此實作,並提供三個參考資料: Bottom-up Mergesort: A Detailed Analysis Wolfgang Panny, Helmut Prodinger Algorithmica 14(4):340--354, October 1995 https://doi.org/10.1007/BF01294131 https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.6.5260 The cost distribution of queue-mergesort, optimal mergesorts, and power-of-two rules Wei-Mei Chen, Hsien-Kuei Hwang, Gen-Huey Chen Journal of Algorithms 30(2); Pages 423--448, February 1999 https://doi.org/10.1006/jagm.1998.0986 https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.4.5380 Queue-Mergesort Mordecai J. Golin, Robert Sedgewick Information Processing Letters, 48(5):253--259, 10 December 1993 https://doi.org/10.1016/0020-0190(93)90088-q https://sci-hub.tw/10.1016/0020-0190(93)90088-Q 稍後詳閱。 ### 參數欄位 ```c= __attribute__((nonnull(2,3))) void list_sort(void *priv, struct list_head *head, list_cmp_func_t cmp) { ``` 第一行 `__attribute__((nonnull(2,3)))` 是告訴 gcc 這個函式應該具有什麼屬性,在編譯時期就可以檢查程式碼的正確性,詳情在下方[\_\_attribute\_\_ 的作用](https://hackmd.io/0oQNR91SSRKprDpLbObf6w?view#__attribute__-%E7%9A%84%E4%BD%9C%E7%94%A8)說明。 參數說明: - `@priv`: private data,`list_sort()` 並不會直接操作,只會把 `priv` 傳遞給 `@cmp`,如果 `@cmp` 不需要使用 `priv`,可以直接傳入 `NULL` 作為 `priv`,但是 `@cmp` 仍必須讓第一個參數去接收 `priv`。(可以參考 [linux/drivers/gpu/drm/i915/gvt/debugfs.c](https://github.com/torvalds/linux/blob/57fa2369ab17d67e6232f85b868652fbf4407206/drivers/gpu/drm/i915/gvt/debugfs.c) 如何使用 `list_sort` 以及 `mmio_offset_compare`) - `@head`:要排序的串列的 Head。 - `@cmp`:元素比較函數,為 function pointer,其型態 `list_cmp_func_t` 宣告如下: ```c typedef int __attribute__((nonnull(2,3))) (*list_cmp_func_t)(void *, const struct list_head *, const struct list_head *); ``` `@cmp` 的回傳值必須是 `int` 型態,`@cmp` 的第一個參數(型態為 `void*`)負責接收 `@priv`。第二、三個參數 `@a`、`@b` 為串列的 Node 的 `list_head`。如果 `@a` 應該排在 `@b` 之後,`@cmp` 必須回傳 > 0(例如 `@a > @b`,並且升冪排序);反之,如果 `@a` 應該排在 `@b` 之前(即順序不變),`@cmp` 必須回傳 <= 0。`list_sort` 是 stable sort,因此不需要區分 `@a < @b` 和 `@a == @b` 的情況。 :::info 這裡原文提到 "This is compatible with two styles of @cmp function ... e.g. `plug_ctx_cmp()` in block/blk-mq.c." 但是在 [block/blk-mq.c](https://github.com/torvalds/linux/blob/a12821d5e012a42673f6fe521971f193441d8aa4/block/blk-mq.c) 裡根本沒有 `plug_ctx_cmp()`,我也不知道是什麼機制允許 `@cmp` 為兩個參數的 function pointer,或許我理解錯 "two styles of @cmp function" 的意思? ::: ### 初始化 merge sort ```c=4 struct list_head *list = head->next, *pending = NULL; size_t count = 0; /* Count of pending */ if (list == head->prev) /* Zero or one elements */ return; /* Convert to a null-terminated singly-linked list. */ head->prev->next = NULL; ``` 首先檢查 list 是否至少有兩個 Node,接下來讓最後一個 Node 的 `next` 指向 `NULL`,使其變成 singly linked list。`prev` 指標不再指向前一個 Node,而是另有用途。 ```graphviz digraph ele_list { rankdir=LR; node[shape=record]; e2 [label="<top>Node 1|{<left>prev|<right>next}", style="bold", pos="0,1!"]; e3 [label="<top>Node 2|{<left>prev|<right>next}", style="bold"]; e4 [label="<top>...|{<left>|<right>}", style="bold"]; e5 [label="<top>Node N|{<left>prev|<right>next}", style="bold"]; NULL [label=NULL, style="bold", color=red, fontcolor=red]; e3:left -> e2:top; e4:left -> e3:top; e5:left -> e4:top; e5:right -> NULL [color=red]; e2:right -> e3:top; e3:right -> e4:top; e4:right -> e5:top; p [label="pending", style=dashed, color=grey]; p -> NULL:n [style=dashed, color=grey]; l [label="list", style=dashed, color=grey]; l -> e2:n [style=dashed, color=grey]; } ``` ### 走訪整個 list 同時執行 merge sort ```c=12 do { size_t bits; struct list_head **tail = &pending; /* Find the least-significant clear bit in count */ for (bits = count; bits & 1; bits >>= 1) tail = &(*tail)->prev; /* Do the indicated merge */ if (likely(bits)) { struct list_head *a = *tail, *b = a->prev; a = merge(priv, cmp, b, a); /* Install the merged result in place of the inputs */ a->prev = b->prev; *tail = a; } /* Move one element from input list to pending */ list->prev = pending; pending = list; list = list->next; pending->next = NULL; count++; } while (list); ``` 參照上方 do while 迴圈與註解可以看出 merge sort 實做的一些想法: - `pending` 是已經排序好但尚未被 merge 的 sublists 的 list,亦稱為 "list of list"。 - 每個排序好的 sublist 的長度正好為 $2^k$。 `bits` 用於判斷何時必須 merge 相鄰的若干個 Node,其目的是檢查目前的 sublist 是否湊滿 $2^k$ 個 Node,若目前有 $2^k$ 個 Node,`if(likely(bits))` 就會成立(`likely()` 用於優化編譯後的組合語言,稍後在[ likely 與 unlikely 巨集](https://hackmd.io/0oQNR91SSRKprDpLbObf6w?view#likely-%E8%88%87-unlikely-%E5%B7%A8%E9%9B%86)解釋),並呼叫 `merge` 來合併最新的兩個 pending sublists,然後讓 `list` 指向下一個 Node;若不到 $2^k$ 能 `merge`,就只讓 `list` 指向下一個 Node。 `for (bits = count; bits & 1; bits >>= 1)` 會持續將 `bits` 右移,直到遇到由右向左數來的的第一個 clear bit,若 for 迴圈完畢後 `bits` 仍不為 0,就 merge 兩個長度一樣的 pending sublists。只閱讀程式碼太抽象,我列舉 `count` 等於 0 到 11 的狀態: | count(十進位) | count(二進位) | 所有 sublist 的狀態 | | --------------- | ------------------- |:------------------------ | | 0 | $0000$ | [1] | | 1 | $000\color{red}{1}$ | [1,1] | | 2 (merge) | $0010$ | [1,1,1] -> [2,1] | | 3 | $00\color{red}{11}$ | [2,1,1] | | 4 (merge) | $0100$ | [2,1,1,1] -> [2,2,1] | | 5 (merge) | $010\color{red}{1}$ | [2,2,1,1] -> [4,1,1] | | 6 (merge) | $0110$ | [4,1,1,1] -> [4,2,1] | | 7 | $0\color{red}{111}$ | [4,2,1,1] | | 8 (merge) | $1000$ | [4,2,1,1,1] -> [4,2,2,1] | | 9 (merge) | $100\color{red}{1}$ | [4,2,2,1,1] -> [4,4,1,1] | | 10 (merge) | $1010$ | [4,4,1,1,1] -> [4,4,2,1] | | 11 (merge) | $10\color{red}{11}$ | [4,4,2,1,1] -> [8,2,1,1] | 上表中第一個欄位是 `count` 在 do while 迴圈中每一次迭代的值(0 到 n-1),後面緊接著 (merge) 代表在該次迭代中有呼叫 `merge` 來合併 sublist。 第二個欄位是以二進位表示 `count`,可以注意到由右到左的連續 set bits 被標記為紅色,代表被 for 迴圈右移而被移除。 第三個欄位為每次 do while 迴圈迭代完成後,所有 pending sublist 的狀態,每個以逗號隔開的數字代表該 sublist 的 Node 數量,所有數字加起來會等於 `count+1`,箭號代表 merge 後 pending lists 變為箭號後的狀態。舉 `count == 5` 為例,`[2,2,1,1]` 代表第一、二個 Node 屬於第一個 sublist,第三、四個 Node 屬於第二個 sublist,第五個 Node 自成第三個 sublist,第六個 Node 自成第四個 sublist。此時因為第一、二個 sublist 長度皆為 $2^1$,且第一、二個 sublist 後面的 Node 數量也為 $2^1$ 個,符合 $2:1$ balanced merge,所以 merge 這兩個 sublist,因此狀態變為 `[4,1,1]`。 解釋完 `bits` 的奧妙機制後,接下來談實作上如何是如何儲存 pending sublist 狀態,最關鍵的手法是利用 `prev` 指向每個 sublist 的第一個 Node。在每次 do while 迭代,`list` 會指向第 `count` 個 Node,`pending` 會指向前一個 Node,indirect pointer `tail` 會指向 `&pending` 並在 `bits` 向右移時,一直指向 `tail = &(*tail)->prev` 藉以把自己移動到可能將被 merge 的 sublist,在 sublist 被 merge 後更新 `prev`。 圖示: **執行完 count = 0 的迭代:** Node 1 自成一個 sublist,所以所有 sublists 的狀態為 `[1]`。 ```graphviz digraph ele_list { rankdir=LR; node[shape=record]; e2 [label="<top>Node 1|{<left>prev|<right>next}", style="bold"]; e3 [label="<top>Node 2|{<left>prev|<right>next}", style="bold"]; e4 [label="<top>Node 3|{<left>prev|<right>next}", style="bold"]; e5 [label="<top>...|{<left>|<right>}", style="bold"]; e6 [label="<top>Node N|{<left>prev|<right>next}", style="bold"]; NULL [label=NULL, style="bold"]; NULL [label=NULL, style="bold"]; subgraph cluster_0 { style=filled; color="#a6cee3"; label="sublist 1"; e2; } e2:left -> NULL [color=red]; e3:left:n -> e2:top; e4:left:n -> e3:top; e5:left:n -> e4:top; e6:left:n -> e5:top; e2:right -> NULL [color=red]; e3:right -> e4; e4:right -> e5; e5:right -> e6; e6:right:s -> NULL:sw; p [label="pending", style=dashed, color=grey]; p -> e2:nw [style=dashed, color=grey]; l [label="list", style=dashed, color=grey]; l -> e3:nw [style=dashed, color=grey]; } ``` **count = 1** 此時 Node 1 自成一個 sublist、Node 2 也自成一個 sublist,sublists 的狀態為 `[1,1]`。 ```graphviz digraph ele_list { rankdir=LR; node[shape=record]; e2 [label="<top>Node 1|{<left>prev|<right>next}", style="bold"]; e3 [label="<top>Node 2|{<left>prev|<right>next}", style="bold"]; e4 [label="<top>Node 3|{<left>prev|<right>next}", style="bold"]; e5 [label="<top>...|{<left>|<right>}", style="bold"]; e6 [label="<top>Node N|{<left>prev|<right>next}", style="bold"]; NULL [label=NULL, style="bold"]; NULL [label=NULL, style="bold"]; subgraph cluster_0 { style=filled; color="#a6cee3"; label="sublist 1"; e2; } subgraph cluster_1 { style=filled; color="#a6cee3"; label="sublist 2"; e3; } e2:left -> NULL; e3:left:n -> e2 [color=red]; e4:left:n -> e3; e5:left:n -> e4; e6:left:n -> e5; e2:right -> NULL; e3:right -> NULL:sw [color=red]; e4:right -> e5; e5:right -> e6; e6:right -> NULL; p [label="pending", style=dashed, color=grey]; p -> e3:nw [style=dashed, color=grey]; l [label="list", style=dashed, color=grey]; l -> e4:n [style=dashed, color=grey]; } ``` **count = 2** 此時 Node 3 也自成一個 sublist,sublists 的狀態為 `[1,1,1]`。我們發現 Node 1 與 Node 2 為首的 sublist 長度都為 $2^0$,且後面有一個 Node 3,形成 $2:1$,我們 merge 以 Node 1 和 Node 2 為首的 sublists,讓狀態變成 `[2,1]`。長度為 2 的 sublist 即為下圖綠色區域,Node 1、Node 2 是排序好的 singly linked list。 ```graphviz digraph ele_list { rankdir=LR; node[shape=record]; e2 [label="<top>Node 1|{<left>prev|<right>next}", style="bold"]; e3 [label="<top>Node 2|{<left>prev|<right>next}", style="bold"]; e4 [label="<top>Node 3|{<left>prev|<right>next}", style="bold"]; e5 [label="<top>...|{<left>|<right>}", style="bold"]; e6 [label="<top>Node N|{<left>prev|<right>next}", style="bold"]; NULL [label=NULL, style="bold"]; NULL [label=NULL, style="bold"]; subgraph cluster_0 { style=filled; color="#b2df8a"; label="sublist 1"; e2;e3; } subgraph cluster_1 { style=filled; color="#a6cee3"; label="sublist 2"; e4; } e2:left -> NULL [color=red]; e4:left:n -> e2 [color=red]; e5:left:n -> e4; e6:left:n -> e5; e2:right -> e3 [color=red]; e3:right -> NULL; e4:right -> NULL [color=red]; e5:right -> e6; e6:right -> NULL; p [label="pending", style=dashed, color=grey]; p -> e4:nw [style=dashed, color=grey]; l [label="list", style=dashed, color=grey]; l -> e5:n [style=dashed, color=grey]; } ``` **count = 3** sublists 的狀態為 `[2,1,1]`。 ```graphviz digraph ele_list { rankdir=LR; node[shape=record]; l1 [label="<top>Node 1, 2|{<left>|<right>}", style="bold"]; e4 [label="<top>Node 3|{<left>prev|<right>next}", style="bold"]; e5 [label="<top>Node 4|{<left>prev|<right>next}", style="bold"]; e6 [label="<top>...|{<left>|<right>}", style="bold"]; e7 [label="<top>Node N|{<left>prev|<right>next}", style="bold"]; NULL [label=NULL, style="bold"]; NULL [label=NULL, style="bold"]; subgraph cluster_0 { style=filled; color="#b2df8a"; label="sublist 1"; l1; } subgraph cluster_1 { style=filled; color="#a6cee3"; label="sublist 2"; e4; } subgraph cluster_2 { style=filled; color="#a6cee3"; label="sublist 3"; e5; } l1:right -> NULL; l1:left -> NULL; e4:left:n -> l1; e5:left:n -> e4 [color=red]; e6:left:n -> e5; e7:left:n -> e6; e4:right -> NULL; e5:right -> NULL [color=red]; e6:right -> e7; e7:right -> NULL; p [label="pending", style=dashed, color=grey]; p -> e5:nw [style=dashed, color=grey]; l [label="list", style=dashed, color=grey]; l -> e6:nw [style=dashed, color=grey]; } ``` **count = 4** sublist 2 與 sublist 3 長度都為 $2^0$,且後面有 Node 5 形成 $2:1$,我們 merge sublist 2 和 sublist 3,讓狀態由 `[2,1,1,1]` 變成 `[2,2,1]`。 ```graphviz digraph ele_list { rankdir=LR; node[shape=record]; l1 [label="<top>Node 1, 2|{<left>|<right>}", style="bold"]; e4 [label="<top>Node 3|{<left>prev|<right>next}", style="bold"]; e5 [label="<top>Node 4|{<left>prev|<right>next}", style="bold"]; e6 [label="<top>Node 5|{<left>prev|<right>next}", style="bold"]; e7 [label="<top>...|{<left>|<right>}", style="bold"]; e8 [label="<top>Node N|{<left>prev|<right>next}", style="bold"]; NULL [label=NULL, style="bold"]; NULL [label=NULL, style="bold"]; subgraph cluster_0 { style=filled; color="#b2df8a"; label="sublist 1"; l1; } subgraph cluster_1 { style=filled; color="#b2df8a"; label="sublist 2"; e4;e5; } subgraph cluster_2 { style=filled; color="#a6cee3"; label="sublist 3"; e6; } l1:right -> NULL; l1:left -> NULL; e4:left:s -> l1 [color=red]; e6:left:n -> e4:w [color=red]; e7:left:n -> e6; e8:left:n -> e7; e4:right -> e5; e5:right -> NULL; e6:right -> NULL [color=red]; e7:right -> e8; e8:right -> NULL; p [label="pending", style=dashed, color=grey]; p -> e6:nw [style=dashed, color=grey]; l [label="list", style=dashed, color=grey]; l -> e7:nw [style=dashed, color=grey]; } ``` **count = 5...N** 省略 由上方的圖示可以看出 `prev` 串連了每個 sublist 的第一個 Node,透過 `tail = &pending` 以及執行若干次 `tail = &(*tail)->prev` 定位到指定的 sublist。 ### merge 剩餘的 sublist,並重建構 `prev`,恢復成 circular doubly-linked list 前面的 do while 迴圈執行結束後會留下許多長度為 $2^k$ 且由大到小排序的 pending sublist,舉 n = 15 為例,do while 結束後會留下 [8,4,2,1] 4 個 sublist,此時我們需要透過以下 for 迴圈達成:[8,4,2,1] -> [8,4,3] -> [8,7]。最後呼叫 `merge_all` 來合併剩餘的 [8,7],並且將整個 list 恢復成 circular doubly linked list。 ```c=35 /* End of input; merge together all the pending lists. */ list = pending; pending = pending->prev; for (;;) { struct list_head *next = pending->prev; if (!next) break; list = merge(priv, cmp, pending, list); pending = next; } /* The final merge, rebuilding prev links */ merge_final(priv, cmp, head, pending, list); } ``` ### `__attribute__` 的作用 前面閱讀 `list_sort` 原始碼時,在函數的開頭使用了 `__attribute__((nonnull(2,3)))`。為了釐清它的作用,尋找第一手材料,首先進入 gcc 9.x 的 [onlinedocs](https://gcc.gnu.org/onlinedocs/gcc-9.4.0/gcc/),找到第 6 章 [Extensions to the C Language Family](https://gcc.gnu.org/onlinedocs/gcc-9.4.0/gcc/C-Extensions.html#C-Extensions),找到 [6.33 Declaring Attributes of Functions](https://gcc.gnu.org/onlinedocs/gcc-9.4.0/gcc/Function-Attributes.html#Function-Attributes): 在 GNU C 和 C++ 中,您可以使用 Function Attributes(以下簡稱"屬性")為函式指定某些屬性,這些屬性可以幫助編譯器優化函式,或更仔細地檢查程式碼的正確性。您還可以使用屬性來控制函式的 memory placement、code generation options 和 call/return conventions。許多 attribute 是 target-specific 的,例如許多 target 支援定義 interrupt handler functions 的屬性,這些函式通常必須使用特定的 register 來執行和回傳,此類屬性與其對應的 target 將在 6.33 的每個小節描述。但是大部分 target 都支援相當多屬性了,這些通用的屬性在 [6.33.1 Common Function Attributes](https://gcc.gnu.org/onlinedocs/gcc-9.4.0/gcc/Common-Function-Attributes.html#Common-Function-Attributes) 描述。 屬性由函數宣告中的 `__attribute__` 關鍵字引入,接著用雙括號括起想要使用的屬性,若想要指定多個屬性,就在雙括號內用逗點分隔它們,或者在一個屬性之後緊跟另一個。有關屬性語法的確切規則,請參閱 [6.39 Attribute Syntax](https://gcc.gnu.org/onlinedocs/gcc-9.4.0/gcc/Attribute-Syntax.html#Attribute-Syntax)。如果兩個屬性互相不兼容,編譯器會直接忽略屬性並產生警告。 GCC 還支援 [6.34 Variable Attributes](https://gcc.gnu.org/onlinedocs/gcc-9.4.0/gcc/Variable-Attributes.html#Variable-Attributes)、[6.35 Type Attributes](https://gcc.gnu.org/onlinedocs/gcc-9.4.0/gcc/Type-Attributes.html#Type-Attributes)、[6.36 Label Attributes](https://gcc.gnu.org/onlinedocs/gcc-9.4.0/gcc/Label-Attributes.html#Label-Attributes")、[6.37 Enumerator Attributes](https://gcc.gnu.org/onlinedocs/gcc-9.4.0/gcc/Enumerator-Attributes.html#Enumerator-Attributes)、[6.38 Statement Attributes](https://gcc.gnu.org/onlinedocs/gcc-9.4.0/gcc/Statement-Attributes.html#Statement-Attributes)。 讀到這裡我們已經完全瞭解 Function Attributes 的目的與語法,接下來閱讀它如何去搭配 `nonnull` 屬性。在 [6.33.1 Common Function Attributes](https://gcc.gnu.org/onlinedocs/gcc-9.4.0/gcc/Common-Function-Attributes.html#Common-Function-Attributes) 找到 `nonnull` 的段落:`nonnull` 屬性可以應用於使用至少一個 pointer 作為參數的函式,它表示傳入該欄位的參數必須不為 `NULL`,例如以下函式的宣告: ```c void *my_memcpy (void *dest, const void *src, size_t len) __attribute__((nonnull (1, 2))); ``` 宣告表明第 1、2 個參數不能為 `NULL`,也就是 `dest` 和 `src` 為 non-null。如果編譯器確定在標記為 non-null 的參數槽中傳遞了 `NULL`,並且編譯時啟用了 `-Wnonnull` 選項,就會發出警告,見 [Warning Options](https://gcc.gnu.org/onlinedocs/gcc-9.4.0/gcc/Warning-Options.html#Warning-Options)。除非啟用了 `-fno-delete-null-pointer-checks` 選項,否則編譯器還可以根據某些 non-null 參數進行優化。此外啟用 `-fisolate-erroneous-paths-attribute` 選項以使 GCC 把任何傳遞 `NULL` 到 non-null 函數的呼叫轉換為 *traps*,請參閱 [Optimize Options](https://gcc.gnu.org/onlinedocs/gcc-9.4.0/gcc/Optimize-Options.html#Optimize-Options)。 :::info 我不知道 GCC 的 "trap" 是什麼東西,查不到解釋。 > 就是你在計算機組織課程學到的 trap, interrupt, exception > :notes: jserv ::: 如果沒有在 `nonnull` 填入 *arg-index*,則所有 pointer 參數都被標記為 non-null,例如: ```c void *my_memcpy (void *dest, const void *src, size_t len) __attribute__((nonnull)); ``` ### likely 與 unlikely 巨集 在 list_sort 實作中,某些 if 條件判斷會被包在 `likely` 與 `unlikely` 巨集中,其宣告在 [linux/compiler.h](https://github.com/torvalds/linux/blob/master/include/linux/compiler.h),這個巨集有兩種實現(摘錄自 compiler.h、compiler_types.h 與 compiler_attributes.h,忽略其餘無關的程式碼)。 實作一: ```c // compiler_attributes.h #define __aligned(x) __attribute__((__aligned__(x))) #define __section(section) __attribute__((__section__(section))) ``` ```c // compiler_types.h struct ftrace_branch_data { const char *func; const char *file; unsigned line; union { struct { unsigned long correct; unsigned long incorrect; }; struct { unsigned long miss; unsigned long hit; }; unsigned long miss_hit[2]; }; }; struct ftrace_likely_data { struct ftrace_branch_data data; unsigned long constant; }; ``` ```c // compiler.h #if defined(CONFIG_TRACE_BRANCH_PROFILING) \ && !defined(DISABLE_BRANCH_PROFILING) && !defined(__CHECKER__) #define likely_notrace(x) __builtin_expect(!!(x), 1) #define unlikely_notrace(x) __builtin_expect(!!(x), 0) void ftrace_likely_update(struct ftrace_likely_data *f, int val, int expect, int is_constant); #define __branch_check__(x, expect, is_constant) ({ \ long ______r; \ static struct ftrace_likely_data \ __aligned(4) \ __section("_ftrace_annotated_branch") \ ______f = { \ .data.func = __func__, \ .data.file = __FILE__, \ .data.line = __LINE__, \ }; \ ______r = __builtin_expect(!!(x), expect); \ ftrace_likely_update(&______f, ______r, \ expect, is_constant); \ ______r; \ }) /* * Using __builtin_constant_p(x) to ignore cases where the return * value is always the same. This idea is taken from a similar patch * written by Daniel Walker. */ #ifndef likely #define likely(x) (__branch_check__(x, 1, __builtin_constant_p(x))) #endif #ifndef unlikely #define unlikely(x) (__branch_check__(x, 0, __builtin_constant_p(x))) #endif ``` 實作二: ```c #else #define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0) #define likely_notrace(x) likely(x) #define unlikely_notrace(x) unlikely(x) #endif ``` 觀察上述兩種實作,可以得知以下資訊:當 `CONFIG_TRACE_BRANCH_PROFILING` 被定義時才會採用實作一,否則採用實作二。兩種實作的關鍵都是 `__builtin_expect(!!(x), expect);`。 實作一雖然看起寫很多行看起來很複雜,但仔細看看可以發現它比實作二多做了三件事: - 初始化一個 `static struct ftrace_likely_data ______f`,將 `__func__`、`__FILE__`、`__LINE__` 放入 `______f` 中對應的成員。`__aligned` 與 `__section` 是用 macro 再包裝的 [Variable Attributes](https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-Attributes.html#Common-Variable-Attributes)。 - 呼叫 `__builtin_expect(!!(x), expect)` 並把回傳值儲存到 `______r`。 - 呼叫 `ftrace_likely_update(&______f, ______r, expect, is_constant)`,由函式的名子與傳入參數可以得知 `ftrace_likely_update` 用於追蹤每次執行 `likely` 當前所在的函式、原始碼檔名、行號、`__builtin_expect` 的回傳值、預期為 1 或 0、以及傳入的 `x` 是否為常數。 由此推論實作一是用來給開發者分析 likely 與 unlikely 的發生頻率是否如預期所想,畢竟把一個相對不常成立的條件放到 `likely` 會降低效能,有這麼一個 trace 工具很合理,此外 `likely_notrace` 不會追蹤上述資訊。 關於 `__func__`、`__FILE__`、`__LINE__` 參見 [Standard Predefined Macros](https://gcc.gnu.org/onlinedocs/cpp/Standard-Predefined-Macros.html#Standard-Predefined-Macros)。關於 `__builtin_constant_p`、`__builtin_expect` 參見 [Other Built-in Functions Provided by GCC](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html)。 Built-in Function: `long builtin_expect (long exp, long c)` 您可以使用 __builtin_expect 為編譯器提供分支預測的資訊,一般來說您更應該要使用 profile feedback(-fprofile-arcs),因為寫程式的人在預測程式實際執行方式出了名的不準。`builtin_expect` 的回傳值是 `exp` 的值,必須是一個 integral expression,並且預期 `exp == c`。 ```c if (__builtin_expect (x, 0)) foo (); ``` 上方程式碼表示 `x` 較可能為 0,因此 `foo` 不應該常被執行。 ```c if (__builtin_expect (ptr != NULL, 1)) foo (*ptr); ``` 若想要判斷浮點數,可以改用上方的寫法,判斷浮點數的指標是否為 `NULL`。 期望 `__builtin_expect` 的 `exp` 為 1 的機率由 GCC 的 `builtin-expect-probability` 參數控制,預設為 90%。您還可以使用 `__builtin_expect_with_probability` 自行指定機率。如果在迴圈內使用此 built-in,則機率會影響優化過後的迭代次數。 ## 引入 list_sort.c 到 lab-0 我在 queue.c 中使用前置處理器來切換自己實做的排序與 list_sort。如果有 `#define USE_LINUX_LIST_SOR`,就會編譯 `#ifdef` 和 `#else` 之間的程式碼;否則編譯 `#else` 和 `#endif` 之間的程式碼。詳細在:[blueskyson/lab0-c@24782f7](https://github.com/blueskyson/lab0-c/commit/24782f71f33e3f7c4d3abd1d62f64464286f281d)。 我修改後的 queue.c 如下: ```c // in queue.c #define USE_LINUX_LIST_SORT // use linux list_sort #ifdef USE_LINUX_LIST_SORT #define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0) typedef int __attribute__((nonnull(2, 3))) (*list_cmp_func_t) (); __attribute__((nonnull(2, 3, 4))) static struct list_head *merge() __attribute__((nonnull(2, 3, 4, 5))) static void merge_final() __attribute__((nonnull(2, 3))) void list_sort() // custom compare function static int compare() // call list_sort void q_sort(struct list_head *head) { if (!head) return; list_sort(NULL, head, compare); } #else // do top down merge sort void sort_recur(struct list_head **phead) // my merge sort implementation void q_sort(struct list_head *head) #endif ``` ## 比較自己的 sort 與 list_sort 效能落差 打算用 `perf `測試排序 100、1k、10k、100k、1000k 個 Node 的 branches, instructions, context-switches。每筆測試使用 `perf` 重複 10 次,最後畫成表格或折線圖分析我的 sort 與 list_sort 的差異。 使用 `perf` 有個缺點,`sort` 前後的 `ih RAND number` 和 `free` 的過程也會被一併記錄下來,所以用 `perf` 只能看出誰好誰壞,無法單獨計算 `q_sort` 函式耗用的資源。 ### 測試腳本 參考 [Linux 效能分析工具: Perf](http://wiki.csie.ncku.edu.tw/embedded/perf-tutorial) 來設定 `perf`。 在 lab0-c 新增 perf-traces 目錄,目錄結構如下: ``` perf-traces/ ├── 1000000.cmd ├── 100000.cmd ├── 10000.cmd ├── 1000.cmd └── 100.cmd ``` `num.cmd` 代表排序 `num` 個隨機字串,例如: ```cmd # 100.cmd, sort 100 RAND strings option fail 0 option malloc 0 new ih RAND 100 sort free ``` 接下來我寫了一個 bash script 來執行效能測試,這個腳本會讓 `perf` 對每個 `qtest -v 0 -f perf-traces/$i.cmd` 重複執行十次,並將輸出儲存在 `perf-traces/$i_report`。腳本內容如下: ```bash # perf-test.sh declare -a traces=("100" "1000" "10000" "100000" "1000000") for i in "${traces[@]}" do perf stat --repeat 10 -o perf-traces/"$i"_report \ -e branches,instructions,context-switches \ ./qtest -v 0 -f perf-traces/"$i".cmd done ``` 詳細在:[blueskyson/lab0-c@299c0b2](https://github.com/blueskyson/lab0-c/commit/299c0b2c0ca3394a3a00b97515b670e5ad8ec32a)。 接下來執行: ```bash $ make $ ./perf-test.sh ``` 就能得到類似以下的報告: ``` Performance counter stats for './qtest -v 0 -f perf-traces/100.cmd' (10 runs): 28,4176 branches ( +- 0.56% ) 141,1366 instructions # 1.14 insn per cycle ( +- 0.57% ) 0 context-switches 0.0005918 +- 0.0000304 seconds time elapsed ( +- 5.14% ) ``` 最後我將報告整理成表格。 ### 用 perf 測量效能 **我實作的 sort** | node num | time (ms) | branches | instructions | contex-switches | |:-------- | --------------- | --------- | ------------ | --------------- | | 100 | 0.5918 (±5.14%) | 284176 | 1411366 | 0 | | 1000 | 0.8602 (±0.72%) | 758525 | 3574613 | 0 | | 10000 | 5.5165 (±1.43%) | 5830672 | 26608412 | 0 | | 100000 | 77 (±2.19%) | 59741126 | 270658689 | 1 | | 1000000 | 1195 (±0.34%) | 630086164 | 2844184459 | 39 | **list_sort** | node num | time (ms) | branches | instructions | contex-switches | |:-------- | --------------- | --------- | ------------ | --------------- | | 100 | 0.5774 (±4.01%) | 285063 | 1419843 | 0 | | 1000 | 0.8351 (±1.16%) | 747332 | 3557516 | 0 | | 10000 | 4.9556 (±0.27%) | 571656 | 26554288 | 0 | | 100000 | 67 (±1.32%) | 58440294 | 270425912 | 5 | | 1000000 | 1012 (±0.52%) | 614368417 | 2840187614 | 34 | 由上兩表可以看出 `list_sort` 在任何情況下,平均執行時間 (上表的 time (ms)) 都比較少,在 node num 為 100 以外的情況下 branch 和 instructions 也都比較低。這說明了 list_sort 在 node num 大於 100 時使用比我的 merge sort 還要少的指令、更快的完成排序。 ### 測量 `q_sort` 的 Wall Clock Time 因為我想得知 `q_sort` 函式在這兩種實作確切的執行時間,我將測試用的 cmd 檔的 `sort` 改成 `time sort`。並且修改 console.c 的 `do_time` 使得顯示的執行時間顯示到小數點後第六位: ```c // in console.c do_time() report(1, "%.6f", delta); // %.3f to %.6f ``` 然後執行以下 bash 腳本: ```bash # perf-test.sh declare -a traces=("200000" "400000" "600000" "800000" "1000000") for i in "${traces[@]}" do ./qtest -v 1 -f perf-traces/"$i".cmd > perf-traces/dump.txt done ``` 得到一連串執行時間: ```bash $ ./perf-test.sh 0.121041 0.355911 0.483806 0.7128ㄕ 0.934433 ``` 畫成表格: | node num | my sort time | list_sort time | list_sort time 是 my sort 的多少 | |:-------- | ------------ | -------------- | -------------------------------- | | 200k | 121.04 | 108.78 | 89.87% | | 400k | 355.91 | 266.27 | 74.81% | | 600k | 483.81 | 423.91 | 87.61% | | 800k | 712.89 | 636.96 | 89.34% | | 1000k | 934.43 | 761.94 | 81.54% | 將兩者的執行時間畫成圖表: :::spoiler python script ```python import plotly.express as px import plotly.graph_objs as go def readfile(fname): arr = [] file = open(fname, "r") while True: line = file.readline() if not line: break arr.append(round(float(line) * 1000, 2)) return arr fig = go.Figure() fig.update_layout( title="Execution Time", xaxis_title="node number", yaxis_title="time (ms)", template="plotly_white" ) xtics = [200000, 400000, 600000, 800000, 1000000] arr1 = readfile("dump.txt") arr2 = readfile("dump1.txt") fig.add_trace(go.Scatter(x=xtics, y=arr1, mode='markers+text', \ text=arr1, textposition="top right", name="my sort")) fig.add_trace(go.Scatter(x=xtics, y=arr2, mode='markers+text', \ text=arr2, textposition="bottom right", name="list_sort")) fig.show() ``` ::: ![](https://i.imgur.com/d5ikDqk.png) 接下來我把 sort 的所有執行時間同除以 node 數量為 200k 的執行時間,讓第一次執行時間固定為 1.0,然後與 $O(Nlog_2N)$、$O(n)$ 的圖表比較: :::spoiler python script ```python base1 = arr1[0] base2 = arr2[0] for i in range(5): arr1[i] = round(arr1[i] / base1, 2) arr2[i] = round(arr2[i] / base2, 2) fig = go.Figure() fig.update_layout( title="Execution Time", xaxis_title="node number", yaxis_title="time (ms)", template="plotly_white" ) fig.add_trace(go.Scatter(x=xtics, y=arr1, mode='markers+text', \ text=arr1, textposition="top right", name="my sort")) fig.add_trace(go.Scatter(x=xtics, y=arr2, mode='markers+text', \ text=arr2, textposition="bottom right", name="list_sort")) nlog2n = [i * math.log(i, 2) for i in range(1, 11)] fig.add_trace(go.Scatter(x=xtics, y=nlog2n, mode='lines', \ line=dict(dash='dash'), name="O(n * log2 n)")) fig.add_trace(go.Scatter(x=xtics, y=list(range(1,6)), mode='lines', \ line=dict(dash='dash'), name="O(n)")) fig.show() ``` ::: ![](https://i.imgur.com/8JEWB8h.png) ## 在 qtest 提供新的命令 shuffle 首先我觀察 qtest 添加指令的機制,發現 `ADD_COMMAND` 巨集定義如下: ```c #define ADD_COMMAND(cmd, msg) add_cmd(#cmd, do_##cmd, msg) ``` 前置處理器會把 `cmd` 字串串接為 "do_"`cmd`,自動產生 do_ 開頭的函式名稱,透過 `add_cmd` 將這個 do_ 函式和函式說明串接到 console.c 的 `cmd_list`。我在 qtest.c 的 `console_init` 中添加以下程式碼,並且寫了一個 `do_shuffle` 函式: ```c bool do_shuffle() { return show_queue(3); } static void console_init() { ADD_COMMAND(shuffle, "| Do Fisher-Yates shuffle"); } ``` 這樣便新增了一個 `shuffle` 指令,功能與 `show` 一樣。接下來模仿 `do_sort`,在執行 shuffle 之前檢查佇列並發出警告訊息,再執行 `q_shuffle`。 ```c bool do_shuffle() { if (!l_meta.l) report(3, "Warning: Calling shuffle on null queue"); error_check(); int cnt = q_size(l_meta.l); if (cnt < 2) report(3, "Warning: Calling shuffle on single node"); error_check(); q_shuffle(l_meta.l); show_queue(3); return !error_check(); } ``` ### `q_shuffle` 實作 我的想法是按照 [Fisher–Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm) 提供的演算法: ```bash -- To shuffle an array a of n elements (indices 0..n-1): for i from n−1 downto 1 do j ← random integer such that 0 ≤ j ≤ i exchange a[j] and a[i] ``` 我一開始想透過操作上述 pseudo code 的 `a[j]` 和 `a[i]` 的 `prev` 與 `next` 來達成 shuffle,但發現如果 `i == j + 1` 時會有例外狀況需要另行處理。後來想到可以直接對調 `a[i]` 和 `a[j]` 的 `value` 就好,這樣就不用判斷例外,而且可以可以用較少的指標操作(完全不用 `list_add`、`list_del`)完成交換。 此外我參考 [XOR Linked List](https://hackmd.io/@kenjin/xor_llist#C-Implementation) 定義 `XOR` 巨集來交換 `value` 指向的位址,只是覺得這樣比較帥,不知道與宣告 `char *tmp` 比起來有哪些優缺點。詳細變更見:[blueskyson/lab0-c@10acbd9](https://github.com/blueskyson/lab0-c/commit/10acbd9fde08ce5cad4a648a905ef5fbbd5f6728)。 ```c #include <stdint.h> /* For swapping two strings in q_shffle */ #define XOR(a, b) (char *)((intptr_t)(a)^(intptr_t)(b)) void q_shuffle(struct list_head *head) { int len = q_size(head); if (len < 2) return; for (struct list_head *p1 = head->prev; len > 1; len--, p1 = p1->prev) { int n = rand() % len; if (n == len - 1) continue; struct list_head *p2 = head->next; for (int i = 0; i < n; i++, p2 = p2->next); char **v1 = &list_entry(p1, element_t, list)->value; char **v2 = &list_entry(p2, element_t, list)->value; *v1 = XOR(*v1, *v2); *v2 = XOR(*v1, *v2); *v1 = XOR(*v1, *v2); } } ``` ## 用 Valgrind 排除 qtest 實作的記憶體錯誤 執行 `make valgrind` 後,並沒有顯示任何 memory leak 的報告,輸出如下: ```bash $ make valgrind # Explicitly disable sanitizer(s) make clean SANITIZER=0 qtest make[1]: Entering directory '/home/lin/Desktop/sysprog2022/lab0-c' rm -f qtest.o report.o console.o harness.o queue.o random.o dudect/constant.o dudect/fixture.o dudect/ttest.o linenoise.o .qtest.o.d .report.o.d .console.o.d .harness.o.d .queue.o.d .random.o.d .dudect/constant.o.d .dudect/fixture.o.d .dudect/ttest.o.d .linenoise.o.d *~ qtest /tmp/qtest.* rm -rf .dudect rm -rf *.dSYM (cd traces; rm -f *~) CC qtest.o CC report.o CC console.o CC harness.o CC queue.o CC random.o CC dudect/constant.o CC dudect/fixture.o CC dudect/ttest.o CC linenoise.o LD qtest make[1]: Leaving directory '/home/lin/Desktop/sysprog2022/lab0-c' cp qtest /tmp/qtest.scGuNm chmod u+x /tmp/qtest.scGuNm sed -i "s/alarm/isnan/g" /tmp/qtest.scGuNm scripts/driver.py -p /tmp/qtest.scGuNm --valgrind --- Trace Points +++ TESTING trace trace-01-ops: # Test of insert_head and remove_head --- trace-01-ops 5/5 +++ TESTING trace trace-02-ops: # Test of insert_head, insert_tail, remove_head, remove_tail, and delete_mid --- trace-02-ops 6/6 +++ TESTING trace trace-03-ops: # Test of insert_head, insert_tail, reverse, and remove_head --- trace-03-ops 6/6 +++ TESTING trace trace-04-ops: # Test of insert_head, insert_tail, size, swap, and sort --- trace-04-ops 6/6 +++ TESTING trace trace-05-ops: # Test of insert_head, insert_tail, remove_head, reverse, size, swap, and sort --- trace-05-ops 6/6 +++ TESTING trace trace-06-ops: # Test of insert_head, insert_tail, delete duplicate, and sort --- trace-06-ops 6/6 +++ TESTING trace trace-07-string: # Test of truncated strings --- trace-07-string 6/6 +++ TESTING trace trace-08-robust: # Test operations on empty queue --- trace-08-robust 6/6 +++ TESTING trace trace-09-robust: # Test remove_head with NULL argument --- trace-09-robust 6/6 +++ TESTING trace trace-10-robust: # Test operations on NULL queue --- trace-10-robust 6/6 +++ TESTING trace trace-11-malloc: # Test of malloc failure on insert_head --- trace-11-malloc 6/6 +++ TESTING trace trace-12-malloc: # Test of malloc failure on insert_tail --- trace-12-malloc 6/6 +++ TESTING trace trace-13-malloc: # Test of malloc failure on new --- trace-13-malloc 6/6 +++ TESTING trace trace-14-perf: # Test performance of insert_tail, reverse, and sort --- trace-14-perf 6/6 +++ TESTING trace trace-15-perf: # Test performance of sort with random and descending orders # 10000: all correct sorting algorithms are expected pass # 50000: sorting algorithms with O(n^2) time complexity are expected failed # 100000: sorting algorithms with O(nlogn) time complexity are expected pass --- trace-15-perf 6/6 +++ TESTING trace trace-16-perf: # Test performance of insert_tail --- trace-16-perf 6/6 +++ TESTING trace trace-17-complexity: # Test if time complexity of q_insert_tail, q_insert_head, q_remove_tail, and q_remove_head is constant Probably constant time ERROR: Probably not constant time Probably constant time ERROR: Probably not constant time --- trace-17-complexity 0/5 --- TOTAL 95/100 make: *** [Makefile:68: valgrind] Error 1 ``` 我再參考 [2020leon](https://hackmd.io/@6649/linux2022-lab0#%E4%BF%AE%E6%AD%A3%E9%8C%AF%E8%AA%A4%EF%BC%88-Valgrind-%EF%BC%89),執行 `valgrind ./qtest -f ./traces/trace-eg.cmd` 也完全沒有如預期輸出任何錯誤訊息。 接著我再按照 lab-0 的作業說明,測試了一個明顯會 memory leak 的程式: ```c // test valgrind #include <stdlib.h> void func(void) { char *buff = malloc(10); } int main(void) { func(); return 0; } ``` 輸出顯示 valgrind 是正常的,可以抓出 memory leak。 ``` $ gcc -g test.c $ valgrind -q --leak-check=full ./a.out ==14940== 10 bytes in 1 blocks are definitely lost in loss record 1 of 1 ==14940== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) ==14940== by 0x10915E: func (test.c:3) ==14940== by 0x109172: main (test.c:6) ==14940== ``` 為何沒有任何 memory leak 還需要再探討。 ### Massif 視覺化 ``` $ valgrind --tool=massif ./qtest -f traces/trace-01-ops.cmd $ massif-visualizer massif.out.* ``` ![](https://i.imgur.com/J2OYPfQ.png)

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully