iLeafy11
    • 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
    # 2021q1 Homework2 (quiz2) contributed by < `iLeafy11` > ###### tags: `linux2021` ## 作業描述 ## 測驗 1 ### 解釋 `include/linux/list.h` 的精簡實作的運作原理 #### Function-like macro: `offsetof` `offsetof` 定義在 `<linux/stddef.h>` 中,用來計算某一個 struct 結構的成員相對於該結構起始地址的偏移量 (offset)。 ```c #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER) ``` ``` 1. 0 2. ((TYPE *)0) 3. ( ((TYPE *)0)->MEMBER ) 4. &( ((TYPE *)0)->MEMBER ) 5. ( (size_t) &( ((TYPE *)0)->MEMBER ) ``` 上述程式碼的 operation 可以拆解成以下: 1. 首先從數值 `0` 開始。 2. 將數值 `0` (`NULL`) 強制轉型成某個 `struct` 結構的指標 `(TYPE *)` 型別,`0` 會被認定是該 `struct` 結構 `TYPE` 的起始地址。 3. 然後再指向該 `struct` 結構型別內的某個成員 `MEMBER`。 4. 取得其地址 (address of `( ((TYPE *)0)->MEMBER )`)。 5. 再將該地址轉型回 `size_t`。 因為起始地址為 `0`,所以 `MEMBER` 的地址也就等於 `MEMBER` 與起始地址 `0` 的偏移 (offset)。 如果將起始地址 `0` 換成不同的數字,那麼就要記得將結果扣掉該數字,這樣才可以算出偏移量 (offset)。 ```c #define offsetof(TYPE, MEMBER) ((size_t) &(((TYPE *)87)->MEMBER) - 87) ``` 上述程式碼實現依賴於早期編譯器,所以不會有 Undefined behavior: dereference a `NULL` pointer 的問題,如今以 C11 的標準來看,`offsetof` 被定義於 gcc 內部實作: ```c #define offsetof(st, m) __builtin_offsetof(st, m) ``` 詳細資料參照: [GCC syntastic extension: Offsetof macro](https://gcc.gnu.org/onlinedocs/gcc/Offsetof.html#Offsetof) 至於 `__builtin_offsetof(st, m)` gcc 的實作,在此不討論,參照: [gcc/cp/parser.c](https://raw.githubusercontent.com/gcc-mirror/gcc/master/gcc/cp/parser.c) #### Function-like macro: `container_of` `container_of() - Calculate address of object that contains address ptr` ```c #define container_of(ptr, type, member) \ __extension__({ \ const __typeof__(((type *) 0)->member) *__pmember = (ptr); \ (type *) ((char *) __pmember - offsetof(type, member)); \ }) ``` `container_of()` macro 使用到 gcc extention `__extension__` ,包含 `__typeof__()` 以及 `offsetof()` 的使用。 `container_of()` 實作的原理: 1. 與上面 `offsetof()` 原理相似,只是從原本第 `4.` 步取地址,變成用 `__typeof__()` 去取得其型別,並宣告該型別的指標 `__pmember` 去接住被複製的 `(ptr)` 的值。 2. `__pmember` 的值減去 `offsetof()` 該結構 `struct` 型別 `type` 的偏移量,就可以得到該結構 `struct` 型別 `type` 其內部成員 `member` 的 container 的地址。 對於為何 `const __typeof__(((type *) 0)->member) *__pmember = (ptr)` 不會產生 Undefined behavior: dereference a `NULL` pointer,~~我們將問題推給 gcc 對於 `typeof` extension 的實作~~,參照: ["Why this 0 in ((type*)0)->member in C?" from Stack Overflow](https://stackoverflow.com/questions/13723422/why-this-0-in-type0-member-in-c) :::info **以 GNU extension `typeof` operator 優化 function-like macro 實作** 若在定義 Macro 時,善用 GNU extension 的 `typeof` operator 可以避免 double evaluation。 **Double evaluation 的範例** 假設我們實作了 function-like macro `max()`,並定義函式 `x()`、`y()` 如下 ```c #define max(a,b) (a < b ? b : a) int x(){ turnLeft(); return 0; } int y(){ turnRight(); return 1; } ``` 呼叫上述實作的 function-like macro `max()` ```c int findmax = max(x(), y()); ``` 將上面呼叫的 function-like macro `max()` 展開來看 ```c int findmax = max(x() < y() ? y() : x()); ``` 我們可以觀察到,`max()` 在被呼叫後,在比較大小 `x() < y()` 的過程中,`x()` 和 `y()` 都會個別被呼叫 1 次,此時 `turnLeft()` 和 `turnRight()` 也各會被執行一次。 因為 `y()` 函式 `return` 的值 `1` 比 `x()` 函式 `return` 的值 `0` 還要大,所以 `y()` 函式會被呼叫第 2 次,於是 `turnRight()` 會再被執行 1 次。 這樣造成的結果就會導致 `max()` 在被呼叫後,`turnLeft()` 被執行 1 次,而 `turnRight` 被執行 2 次的情形。 所以說,如果單純以 [運算表達式 (`expressions`)](https://en.cppreference.com/w/c/language/expressions),只用 `operators` 和 `operands` 去實作 function-like macro,一不小心就有可能產生會造成 double evaluation 的 function-like macro。 **以 GNU extension 的 `typeof` operator 改寫 `max()`** 實作如下: ```c #define max(a,b) \ ({ typeof (a) _a = (a); \ typeof (b) _b = (b); \ _a > _b ? _a : _b; }) ``` 若 `max()` 改用 GNU extension 的 `typeof` operator 實作,即可避免 double evaluation,同時亦可確保型別 (type) 的安全。 在 GNU C 裡面,GNU extension 可以利用 `__auto_type` operator 提供更好的方式實作 `max()`,如下: ```c #define max(a,b) \ ({ __auto_type _a = (a); \ __auto_type _b = (b); \ _a > _b ? _a : _b; }) ``` Reference: * ["What is double evaluation and why should it be avoided?" from Stack Overflow](https://stackoverflow.com/questions/39439181/what-is-double-evaluation-and-why-should-it-be-avoided) * [6.7 Referring to a Type with typeof from C Extensions (Using the GNU Compiler Collection (GCC))](https://gcc.gnu.org/onlinedocs/gcc/Typeof.html#Typeof) ::: #### Data Structure: `struct list_head - Head and node of a doubly-linked list` ```c struct list_head { struct list_head *prev, *next; }; ``` ```graphviz digraph list_head { rankdir=LR; node[shape=record, style=bold]; head [label="{<prev>prev|<next>next}"]; } ``` ```c typedef struct __element { char *value; struct list_head list; } list_ele_t; ``` ```graphviz digraph list { rankdir=LR; node[shape=record, style=bold]; subgraph cluster_0 { node [shape=record]; value [label="{value}"]; head [label="{<prev>prev|<next>next}"]; style=bold; label=list_ele_t } } ``` 簡化成下圖: ```graphviz digraph list_ele { rankdir=LR; node[shape=record]; node [shape=record]; head [label="value|{<left>prev|<right>next}", style=bold]; } ``` #### Function-like macro: `LIST_HEAD - Declare list head and initialize it` ```c #define LIST_HEAD(head) struct list_head head = {&(head), &(head)} ``` ```graphviz digraph list_init { rankdir=LR; node[shape=record]; style=bold node [shape=record]; head [label="value|{<left>prev|<right>next}", style="bold"]; head:right:e -> head:left:w[arrowhead=normal, arrowtail=normal, dir=both]; } ``` `list_entry() - Calculate address of entry that contains list node` ```c #define list_entry(node, type, member) container_of(node, type, member) ``` ```graphviz digraph container { rankdir=LR; node[shape=record]; compound=true; style=bold; subgraph cluster_0 { container [label = "container" style=filled,color=white]; subgraph cluster_1 { node [shape=record]; element [label="|{|}", style="bold"]; label = "member" } } element -> container[ltail=cluster_1, lhead=cluster_0]; } ``` `list_for_each - iterate over list nodes` ```c #define list_for_each(node, head) \ for (node = (head)->next; node != (head); node = node->next) ``` ```graphviz digraph ele_list { rankdir=LR; node[shape=record]; h [label="head", style=dashed, color=grey]; h -> e1:right:w [style=dashed, color=grey]; e1 [label="head node|{<left>prev|<right>next}", style="bold", color=grey]; e2 [label="cat|{<left>prev|<right>next}", style="bold"]; e3 [label="...", style="bold"]; e4 [label="eat|{<left>prev|<right>next}", style="bold"]; e5 [label="fat|{<left>prev|<right>next}", style="bold"]; e1:right:e -> e2:left:w[arrowhead=normal, arrowtail=normal, dir=both]; e2:right:e -> e3:left:w[arrowhead=normal, arrowtail=normal, dir=both]; e3:right:e -> e4:left:w[arrowhead=normal, arrowtail=normal, dir=both]; e4:right:e -> e5:left:w[arrowhead=normal, arrowtail=normal, dir=both]; e5:right:e -> e1:left:w[arrowhead=normal, arrowtail=normal, dir=both]; } ``` :::info TODO: 採用 circular linked list 是基於哪些考量? ::: #### Functions for implementing Merge Sort ### 改進上述程式碼並實作 #### 去除不必要的資料結構 不需要用到佇列 (Queue) 資料結構,此結構無益於 linked list merge sort 的排序,且同時浪費記憶體以及效能。 ```diff typedef struct __element { char *value; - struct __element *next; struct list_head list; } list_ele_t; - typedef struct { - list_ele_t *head; /* Linked list of elements */ - list_ele_t *tail; - size_t size; - struct list_head list; - } queue_t; ``` #### 修改函式 **函式: `list_del()`** `list_del()` 以命名上來說是 **刪除(delete)** list 的某個節點結構,將該節點移出 list 並將釋放其記憶體。然而觀察 `list_del()` 程式碼,其行為卻是只有將目標節點移出 list,而沒有實質上的去 __刪除 (delete)__ 該目標節點。因此這裡我認為比較好的命名方式是 __拆除 (remove)__ 目標節點,函式更名為 `list_remove()`。 ```diff /** - * list_del() - Remove a list node from the list + * list_remove() - Remove a list node from the list * @node: pointer to the node */ - static inline void list_del(struct list_head *node) + static inline void list_remove(struct list_head *node) { struct list_head *next = node->next, *prev = node->prev; next->prev = prev; prev->next = next; } ``` **函式: `list_merge_sort()`** 因為刪除不必要的佇列 (Queue) 結構,所以傳遞佇列 (Queue) 結構參數的函式必須要做修正,改成傳遞 `struct list_head` 的指標參數。 ```diff - void list_merge_sort(queue_t *q) + void list_merge_sort(struct list_head *list) { - if (list_is_singular(&q->list)) - return; - queue_t left; - INIT_LIST_HEAD(&left.list); - list_cut_position(&left.list, &q->list, &get_middle(&q->list)->list); + if (list_is_singular(list)) + return; + struct list_head left; + INIT_LIST_HEAD(&left); + list_cut_position(&left, list, &get_middle(list)->list); struct list_head sorted; list_merge_sort(&left); - list_merge_sort(q); - list_merge(&left.list, &q->list, &sorted); - INIT_LIST_HEAD(&q->list); - list_splice_tail(&sorted, &q->list); + list_merge_sort(list); + list_merge(&left, list, &sorted); + INIT_LIST_HEAD(list); + list_splice_tail(&sorted, list); } ``` **函式: `validate()`** 修改原因及結果同上。 ```diff - static bool validate(queue_t *q) + static bool validate(struct list_head *list) { struct list_head *node; - list_for_each (node, &q->list) { - if (node->next == &q->list) - break; + list_for_each (node, list) { + if (node->next == list) + break; if (strcmp(list_entry(node, list_ele_t, list)->value, list_entry(node->next, list_ele_t, list)->value) > 0) return false; } return true; } ``` **函式: `list_merge()`** 觀察函式 `list_merge()` 裡面,`if` `while` 以及最底下的三元運算子 (ternary operator) 的條件判斷皆與 `list_empty()` 相關。其中,不難發現,前面兩個 `if` 條件式所做的行為,與底下 `while` 以及最底下三元運算子 (ternary operator) 所做的行為自相矛盾。 而這兩個 `if` 條件式內有 `return` 且寫在 `while` 迴圈之前,因此先判斷 `list_empty(lhs)` 而後判斷 `list_empty(rhs)` 的寫法也會在順序上出現問題,因為 `list_empty(lhs)` 與 `list_empty(rhs)` 兩個條件應當是要被同時考量的存在。 舉例來說,在執行 `merge_sort()` 的過程中,會呼叫到 `get_middle()` 來切割 linked list。假設被切割的 linked list 的節點個數為奇數,最終在遞迴的過程中勢必會切割到其中一邊的 linked list 只有 1 個節點,而另一邊 linked list 有 2 個節點的情形。 假設傳進來的參數 `rhs` 接收到的數值是,含有 2 個節點的 linked list 指標,而 `lhs` 接收到的數值是,只含有 1 個節點的 linked list 指標,那麼在不修改題目原始程式碼的情形下,`if` 條件的行為便會造成 `merge_sort()` 的錯誤。 結論是,這兩個 `if` 條件除了是多餘的程式碼 (redundant code) 之外,也會造成 `merge_sort()` 演算法出錯,因此必須予以刪除。 ```diff static void list_merge(struct list_head *lhs, struct list_head *rhs, struct list_head *head) { INIT_LIST_HEAD(head); - if (list_empty(lhs)) { - list_splice_tail(lhs, head); - return; - } - if (list_empty(rhs)) { - list_splice_tail(rhs, head); - return; - } while (!list_empty(lhs) && !list_empty(rhs)) { char *lv = list_entry(lhs->next, list_ele_t, list)->value; char *rv = list_entry(rhs->next, list_ele_t, list)->value; struct list_head *tmp = strcmp(lv, rv) <= 0 ? lhs->next : rhs->next; list_remove(tmp); list_add_tail(tmp, head); } list_splice_tail(list_empty(lhs) ? rhs : lhs, head); } ``` **函式:`merge_sort()`** `merge_sort()` 需要考慮當 list 為空的情形,否則會有 segmentation fault。 ```diff void list_merge_sort(struct list_head *list) { - if (list_is_singular(list)) - return; + if (list_empty(list) || list_is_singular(list)) + return; struct list_head left; struct list_head sorted; INIT_LIST_HEAD(&left); list_cut_position(&left, list, &get_middle(list)->list); list_merge_sort(&left); list_merge_sort(list); list_merge(&left, list, &sorted); INIT_LIST_HEAD(list); list_splice_tail(&sorted, list); } ``` :::info TODO: ::: #### 新增函式 **函式: `list_alloc_add_tail()`** 動態配置記憶體,新增 `list_ele_t` 的節點 `new`,同時再次動態配置記憶體,複製一份 `char *s` 傳遞進來的字串到 `new->value`。最後再藉由 `list_add_tail()` 函式以及傳遞進來的 `struct list_head` 的指標參數,將新的節點 `new` 新增至 circular doubly linked list。 函式執行期間,若動態配置記憶體失敗,則 `return false`。需要注意,如果是在 `strdup()` 動態配置記憶體失敗,則必須歸還在 `list_alloc_add_tail()` 函式內先前已成功動態配置的記憶體 `new`。若兩次的動態配置記憶體皆已成功配置,則在函式執行到最後時 `return true`。 此實作手法類似於 [`quiz1`](https://hackmd.io/g1_AyhXXRRmwtRh965SKLw?view) 的 `q_insert_head()` 以及 `q_insert_tail` 的實作。 ```c bool list_alloc_add_tail(struct list_head *head, char *s) { list_ele_t *new = malloc(sizeof(list_ele_t)); if (!new) return false; new->value = strdup(s); if (!new->value) { free(new); return false; } list_add_tail(&new->list, head); return true; } ``` **Function-like macro: `list_for_each_safe()`** 為了能夠實作 `list_free()`,刪除整個 list 的函式,所以參考了 [linux/list.h](https://github.com/torvalds/linux/blob/master/include/linux/list.h) 的實作 `list_for_each_safe()`。 之所以使用 `list_for_each_safe()` 而非 `list_for_each()` 的原因是,使用 `list_for_each()` 會在刪除節點的過程中將存取下個節點的指標一併抹除,所以為了避免在 iterate over a list 的過程中存取不到下個節點,於是選擇採用此實作。 ```c /** * Shamelessly copy from linux/list.h * list_for_each_safe - iterate over a list safe against removal of list entry * @pos: the &struct list_head to use as a loop cursor. * @n: another &struct list_head to use as temporary storage * @head: the head for your list. */ #define list_for_each_safe(pos, n, head) \ for (pos = (head)->next, n = pos->next; pos != (head); \ pos = n, n = pos->next) ``` **函式: `list_free()`** 利用 `list_for_each_safe()`,走訪整個 linked list 的所有節點,然後再利用 `list_entry()` ,找到在 container (`list_ele_t` structure) 內部以及 container 本身被動態配置的記憶體,然後再將其釋放 (`free`) 掉。 ```c void list_free(struct list_head *head) { struct list_head *curr, *next; list_for_each_safe(curr, next, head) { list_remove(curr); list_ele_t *target = list_entry(curr, list_ele_t, list); free(target->value); free(target); } } ``` #### 修改主程式以及編譯執行 **主程式:`main()`** ```diff int main(void) { FILE *fp = fopen("cities.txt", "r"); if (!fp) { perror("failed to open cities.txt"); exit(EXIT_FAILURE); } - queue_t *q = q_new(); + LIST_HEAD(list); char buf[256]; while (fgets(buf, 256, fp)) { - q_insert_head(q, buf); + list_alloc_add_tail(&list, buf); } fclose(fp); - list_merge_sort(q); - assert(validate(q)); - q_free(q); + list_merge_sort(&list); + assert(validate(&list)); + list_free(&list); return 0; } ``` ``` $ gcc -Wall -o merge_sort merge_sort.c $ ./merge_sort ``` #### 檢查是否有 memory leak ``` $ valgrind --leak-check=full ./merge_sort ==19101== Memcheck, a memory error detector ==19101== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al. ==19101== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info ==19101== Command: ./merge_sort ==19101== ==19101== ==19101== HEAP SUMMARY: ==19101== in use at exit: 0 bytes in 0 blocks ==19101== total heap usage: 187,656 allocs, 187,656 frees, 4,529,436 bytes allocated ==19101== ==19101== All heap blocks were freed -- no leaks are possible ==19101== ==19101== For lists of detected and suppressed errors, rerun with: -s ==19101== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) ``` ### 將 lib/list_sort.c 的實作抽離為可單獨執行 :::info ::: #### compile & execution :::info TODO:附上解釋 ::: #### 設計實驗 & 效能分析 ### 與 quiz1 作效能比較 (linked list merge_sort vs linked list quick_sort)\ ??用相同的資料結構?? :::info TODO:用相同資料結構刻出 QuickSort 並比較效能 ::: --- ## 測驗 2 考慮函式 `func()` 接受一個 16 位元無號整數 N,並回傳小於或等於 N 的 power-of-2。 ```c uint16_t func(uint16_t N) { /* change all right side bits to 1 */ N |= N >> 1; N |= N >> 2; N |= N >> 4; N |= N >> 8; return (N + 1) >> 1; } ``` 其實上述程式碼在做的事情是,就是 Find most significant (left-most) bit。 ### 解釋 `func()` 的運作原理 ```c N |= N >> 1; N |= N >> 2; N |= N >> 4; N |= N >> 8; ``` 上述程式碼的作用是,將所有比 N 的 most significant (left-most) bit 位置還要低的 bit 通通變成 1。原理是 `N |= N >> 1` 會將 most significant (left-most) bit 的下一個 bit 利用 `bitwise or` 也變成是 1,而 `N |= N >> 2` 會利用 `N |= N >> 1` 的結果將 most significant (left-most) bit 的下一個 bit 的後面兩個 bit 也變成是 1,以此類推。 因為函式 func 是接受一個 16 位元無號整數 N,所以 right shift 的最大值就會是 8。 * Example: N = (1000 0000 0000 0000)<sub>2</sub> = (32768)<sub>10</sub> ```c 1000 0000 0000 0000 or 0100 0000 0000 0000 (right-shifted by 1) is 1100 0000 0000 0000 ``` ```c 1100 0000 0000 0000 or 0011 0000 0000 0000 (right-shifted by 2) is 1111 0000 0000 0000 ``` ```c 1111 0000 0000 0000 or 0000 1111 0000 0000 (right-shifted by 4) is 1111 1111 0000 0000 ``` ```c 1111 1111 0000 0000 or 0000 0000 1111 1111 (right-shifted by 8) is 1111 1111 1111 1111 ``` :::info TODO: 描述並解釋 ::: 考慮到 `func()` 最後 `return` 的值為 `(N + 1) >> 1`,若 `N` 的值 = (1000 0000 0000 0000)<sub>2</sub> = (32768)<sub>10</sub>,推測在 `(N + 1)` 時有可能會產生 overflow。 稍微實作實驗一下 `func.c`: ```c #include <stdio.h> #include <stdint.h> uint16_t func(uint16_t N) { N = N | N >> 1; N = N | N >> 2; N = N | N >> 4; N = N | N >> 8; return (N + 1) >> 1; } int main(int argc, char *argv[]) { printf("%u\n", func(32768)); return 0; } ``` 執行時沒有發現什麼問題。 ``` $ gcc -Wall -g -o func func.c $ ./func 32768 ``` 使用 Address Sanitizer 也沒有報錯。 ``` $ clang -fsanitize=address -O1 -fno-omit-frame-pointer -g test.c $ ./a.out ``` 用 gcc 加入參數 `-Wconversion` 編譯時,就會報錯。 ``` $ gcc -o func -Wconversion func.c test.c: In function ‘func’: test.c:10:20: warning: conversion from ‘int’ to ‘uint16_t’ {aka ‘short unsigned int’} may change value [-Wconversion] 10 | return (N + 1) >> 1; | ~~~~~~~~^~~~ ``` 查看 C99 規格書。 :::info **C99 Standard - §6.3.1.1** If an `int` can represent all values of the original type, the value is converted to an `int`; otherwise, it is converted to an `unsigned int`. These are called the integer promotions. All other types are unchanged by the integer promotions. ::: 因此是不會發生 overflow 的。在 **C99 Standard** 中,有定義所謂 "integer promotions" 的行為,所以 `return (N + 1) >> 1;` 這段程式碼實際上會等同於: ```c return (uint8_t)((int)(N + 1) >> 1); ``` 用 gcc 編譯查看其 Assembly 可發現其端倪。 ``` $ gcc -S func.c -o func.s $ cat func.s ``` ```diff .file "func.c" .text .globl func .type func, @function func: .LFB6: .cfi_startproc endbr64 pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl %edi, %eax movw %ax, -4(%rbp) movzwl -4(%rbp), %eax shrw %ax orw %ax, -4(%rbp) movzwl -4(%rbp), %eax shrw $2, %ax orw %ax, -4(%rbp) movzwl -4(%rbp), %eax shrw $4, %ax orw %ax, -4(%rbp) movzwl -4(%rbp), %eax shrw $8, %ax orw %ax, -4(%rbp) movzwl -4(%rbp), %eax addl $1, %eax + sarl %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE6: .size func, .-func .section .rodata .LC0: ... ``` :::info * `eax` is a 32 bit register. * `sar` right shifts (signed divides) a byte, word, or long value for a count specified by an immediate value and stores the quotient in that byte, word, or long respectively. The second variation right shifts by a count value specified in the CL register. sar rounds toward negative infinity; the high-order bit remains unchanged. notes: CSAPP: 3/E chapter 3 ::: 不過如果是在不同的機器上運作,便不能確定上述程式碼是安全的,因為 32-bit 的 `int` type 只會在有 64-bit 暫存器的機器上實現。所以我們最好還是把數值操作限縮在 `uint16_t` 大小的資料。 從 C99 規格書中可以得知 right-shift 為 well-defined behavior。 :::info **C99 Standard - §6.5.7** The result of **E1 >> E2** is **E1** right-shifted **E2** bit positions. If **E1** has an unsigned type or if **E1** has a signed type and a nonnegative value, the value of the result is the integral part of the quotient of **E1** / 2<sup>**E2**</sup>. If **E1** has a signed type and a negative value, the resulting value is implementation-defined. ::: 所以程式碼可以改寫如下: ```diff - return (N + 1) >> 1; + return (N >> 1) + 1; ``` ### Linux 核心的 round up/down power of 2 的運作機制 :::info TODO ::: --- ## 測驗 3 ### 解釋 `bitcpy()` 程式碼運作原理 #### 傳遞參數 ```c void bitcpy(void *_dest, /* Address of the buffer to write to */ size_t _write, /* Bit offset to start writing to */ const void *_src, /* Address of the buffer to read from */ size_t _read, /* Bit offset to start reading from */ size_t count) ``` [題目敘述](https://hackmd.io/@sysprog/linux2021-quiz2#%E6%B8%AC%E9%A9%97-3) * `input/_src`: 長度為 8 個 uint8 陣列 (總共 64 位元)。同時也是 `input` 的起始地址。 * `output/_dest`: 長度為 8 個 uint8 陣列 (總共 64 位元)。同時也是 `output` 的起始地址。 * `_write`: 從 `_dest` 的第 `_write` 個位元開始寫入 `_count` 位元數。 * `_read`: 從 `_src` 的第 `_read` 個位元開始讀取 `_count` 位元數。 * `count`: 讀取/寫入的位元數。 #### 變數 `read_lhs` `read_rhs` `source` `write_lhs` `write_rhs` `dest` 的意義 ```c size_t read_lhs = _read & 7; size_t read_rhs = 8 - read_lhs; const uint8_t *source = (const uint8_t *) _src + (_read / 8); ``` * `read_lhs`: `_read & 7` 可以視為是 `_read % 8`。決定在一個 byte 的大小內,從左邊數來的第 `read_lhs` 個 bit 開始讀取。 * `read_rhs`: 決定在一個 byte 的大小內,從右邊數來的第 `read_rhs` 個 bit 開始讀取。 * `source`: 指向 `uint8_t` 的指標,指標數值加 1 等同於指標指向下一個 8 個 bit 的起始位置。其值以 `_src` 為起始地址,加上 `(_read / 8)` 來決定從哪一個 byte 開始讀取。如下圖: ```graphviz digraph{ node [shape=plaintext, fontsize=60]; { node [shape=record, fontsize=50, width=30, fixsize=true]; bit [label="<p0>1111 1111 | <p1>1111 1111 | <p2>1111 1111 | <p3>1111 1111 | <p4>1111 1111 | <p5>1111 1111 | <p6>1111 1111 | <p7>1111 1111", style=bold]; _src [label="<p0>_src + 0 | <p1>_src + 1 | <p2> _src + 2 | <p3>_src + 3 | <p4>_src + 4 | <p5>_src + 5 | <p6>_src + 6 | <p7> _src + 7"]; } source [label="source", style=dashed]; edge [color=grey, style=bold]; source -> _src:p0; source -> _src:p1; source -> _src:p2; source -> _src:p3; source -> _src:p4; source -> _src:p5; source -> _src:p6; source -> _src:p7; edge [color=black, style=bold]; _src:p0 -> bit:p0; _src:p1 -> bit:p1; _src:p2 -> bit:p2; _src:p3 -> bit:p3; _src:p4 -> bit:p4; _src:p5 -> bit:p5; _src:p6 -> bit:p6; _src:p7 -> bit:p7; } ``` ```c size_t write_lhs = _write & 7; size_t write_rhs = 8 - write_lhs; uint8_t *dest = (uint8_t *) _dest + (_write / 8); ``` * `write_lhs`: 原理同 `read_lhs`。決定在一個 byte 的大小內,從左邊數來的第 `write_lhs` 個 bit 開始寫入。 * `write_rhs`: 原理同 `read_rhs`。決定在一個 byte 的大小內,從右邊數來的第 `write_rhs` 個 bit 開始寫入。 * `dest`: 原理同 `source`。以 `_dest` 為起始地址,加上`(_write / 8)` 來決定從哪一個 byte 開始寫入。如下圖: ```graphviz digraph{ node [shape=plaintext, fontsize=60]; { node [shape=record, fontsize=50, width=30, fixsize=true]; bit [label="<p0>1111 1111 | <p1>1111 1111 | <p2>1111 1111 | <p3>1111 1111 | <p4>1111 1111 | <p5>1111 1111 | <p6>1111 1111 | <p7>1111 1111", style=bold] _dest [label="<p0>_dest + 0 | <p1>_dest + 1 | <p2> _dest + 2 | <p3>_dest + 3 | <p4>_dest + 4 | <p5>_dest + 5 | <p6>_dest + 6 | <p7> _dest + 7"]; } edge [color=grey, style=bold]; dest -> _dest:p0; dest -> _dest:p1; dest -> _dest:p2; dest -> _dest:p3; dest -> _dest:p4; dest -> _dest:p5; dest -> _dest:p6; dest -> _dest:p7; edge [color=black, style=bold]; _dest:p0 -> bit:p0; _dest:p1 -> bit:p1; _dest:p2 -> bit:p2; _dest:p3 -> bit:p3; _dest:p4 -> bit:p4; _dest:p5 -> bit:p5; _dest:p6 -> bit:p6; _dest:p7 -> bit:p7; } ``` #### `bit_mask` 的使用 ```c static const uint8_t read_mask[] = { 0x00, /* == 0 00000000b */ 0x80, /* == 1 10000000b */ 0xC0, /* == 2 11000000b */ 0xE0, /* == 3 11100000b */ 0xF0, /* == 4 11110000b */ 0xF8, /* == 5 11111000b */ 0xFC, /* == 6 11111100b */ 0xFE, /* == 7 11111110b */ 0xFF /* == 8 11111111b */ }; static const uint8_t write_mask[] = { 0xFF, /* == 0 11111111b */ 0x7F, /* == 1 01111111b */ 0x3F, /* == 2 00111111b */ 0x1F, /* == 3 00011111b */ 0x0F, /* == 4 00001111b */ 0x07, /* == 5 00000111b */ 0x03, /* == 6 00000011b */ 0x01, /* == 7 00000001b */ 0x00 /* == 8 00000000b */ }; ``` 可以觀察到其實 `read_mask[]` 和 `write_mask[]` 的內容其實是一樣的,只是順序是倒過來的而已。所以其中一個 `bit_mask` 其實是多餘的。 #### 迴圈終止條件 & 讀取/寫入 bit 實作 bit_copy ```c while (count > 0) { ... size_t bitsize = (count > 8) ? 8 : count; ... count -= bitsize; } ``` `while` 迴圈會在 `count` 為 `0` 的時候,也就是所有的 bit 被複製完的時候離開 `while` 迴圈。 考量到 `count` 不一定為 `8` 的倍數,因此宣告一個 `bitsize` 變數,其值為迴圈的一次循環中,讀取/寫入的 bit 的個數,一次最多為 `8` 個 bit。而當迴圈執行最後一次時,`bitsize` 的值便是 `count` 除以 `8` 的餘數。 考慮到以下 bitwise operation 來讀取/寫入 bit,以實作 bit_copy。 * `X |= 0 -> X` * `X &= 1 -> X` #### 讀取 ```c uint8_t data = *source++; size_t bitsize = (count > 8) ? 8 : count; if (read_lhs > 0) { data <<= read_lhs; if (bitsize > read_rhs) data |= (*source >> read_rhs); } ``` 1. 首先 `data` 複製讀取的 `*source` 的起始 byte 的資料,大小為 1 byte,也就是 8 個 bit。`*source` 被讀完 1 個 byte 之後前往下一個 byte,也就是下一個 8-bit 的起始位置。 2. 在該 byte 內,如果要讀取的起始 bit 的位置沒有對齊讀進來的 byte 的起始位置,也就是當 `read_lhs` 不為 `0` 的時候,則讀取要從左邊數來的第 `read_lhs` 個 bit 開始讀取。所以 `data` 必須要左移 `read_lhs` 個 bit,`data` 右邊的 `read_lhs` 個 bit 會補上 `0`。 3. `bitsize > read_rhs` 的意義是,是否所有要讀取的 bit 都已經完全讀取結束,如果沒有完全讀取結束,則需要跨兩個 byte,再從已前進至下一個 byte 位置的 `*source`,將其值右移 `read_lhs` 個 bit,如此才能複製前 `read_lhs` 個 bit 至 `data` 剛剛被清 `0` 的空間。 ```c if (bitsize < 8) data &= read_mask[bitsize]; ``` 4. `bitsize < 8` 會意味著 `while` 迴圈的執行是否已經到達最後一次,若是最後一次,則利用 `read_mask` 做為遮罩,將多餘讀取的 bit 抹除掉。 ```graphviz digraph { node [shape=plaintext, fontsize=60]; text [label="Assume read_lhs = 3"]; { node [shape=record, fontsize=50, width=30, fixsize=true]; next [label="<p0>X | <p1>X | <p2>X | <p3>X | <p4>X | <p5>X | <p6>X | <p7>X", style=bold] curr [label="<p0>X | <p1>X | <p2>X | <p3>X | <p4>X | <p5>X | <p6>X | <p7>X", style=bold]; prev [label="<p0>X | <p1>X | <p2>X | <p3>X | <p4>X | <p5>0 | <p6>0 | <p7>0", style=bold]; } { rank=same; prev; "" "left-shift by 3" } edge [color=white, style=bold]; text -> prev; prev:p0 -> curr:p0; prev:p7 -> curr:p7; curr:p0 -> next:p0; } ``` ``` read --- XXXXXXXX data copy from *source --- if read_lhs = 3, read_rhs = 5, bitsize <= read_rhs XXXXX000 data left-shift by read_lhs --- if read_lhs = 3, read_rhs = 5, bitsize > read_rhs XXXXX000 data left-shift by read_lhs or 00000XXX *source right-shift by read_rhs is XXXXXXXX --- if bitsize = 6 XXXXXXXX data and 11111100 read_mask[bitsize] is XXXXXX00 --- read end --- ``` #### 寫入 ```c uint8_t original = *dest; uint8_t mask = read_mask[write_lhs]; if (bitsize > write_rhs) { /* Cross multiple bytes */ *dest++ = (original & mask) | (data >> write_lhs); original = *dest & write_mask[bitsize - write_rhs]; *dest = original | (data << write_rhs); } else { // Since write_lhs + bitsize is never >= 8, no out-of-bound access. mask |= write_mask[write_lhs + bitsize]; *dest++ = (original & mask) | (data >> write_lhs); } ``` ``` write --- XXXXXXXX original copy from *dest --- if write_lhs = 6 11111100 mask = read_mask[write_lhs] --- if write_rhs = 2, bitsize = 3 XXXXXXXX original and 11111100 mask is XXXXXX00 original & mask 000000XX data right-shift by write_lhs XXXXXX00 original & mask or 000000XX data right-shift by write_lhs XXXXXXXX ``` ### 重寫為同樣功能但效率更高的實作 #### 改善 bit_mask 的實作 #### :::info unaligned/aligned memory memcpy() ::: #### :::info TODO ::: ### Linux 核心對於 `bit_cpy` 的實作 :::info TODO ::: --- ## 測驗 4 ### 解釋字串駐留 (String Interning) 運作原理 https://hackmd.io/@sysprog/linux2021-quiz2#%E6%B8%AC%E9%A9%97-4 :::info TODO ::: ### 解釋程式碼運作原理 #### `cstr.h` **`enum`** ```c enum { CSTR_PERMANENT = 1, CSTR_INTERNING = 2, CSTR_ONSTACK = 4, }; ``` * `CSTR_PERMANENT` - 表示字串儲存在 heap 的狀態。 * `CSTR_INTERNING` - 表示儲存字串的空間作為 string interning 使用的狀態。 * `CSTR_ONSTACK` - 表示字串儲存在 stack 的狀態。 **`struct`** ```c typedef struct __cstr_data { char *cstr; uint32_t hash_size; uint16_t type; uint16_t ref; } * cstring; ``` * `cstr` - 儲存字串的空間。 * `hash_size` - 當字串的狀態為 `CSTR_ONSTACK`,`hash_size` 的值為字串的長度 (`size`),否則其值為 hash values (`hash`)。 * `type` - 當前字串儲存在的空間的狀態。 * `ref` - reference counting。 `cstring` 為一個指向 `struct __cstr_data` 結構的指標。 ```c typedef struct __cstr_buffer { cstring str; } cstr_buffer[1]; ``` * str - `cstring` type,為指向 `struct __cstr_data` 結構的指標。 **Function-like macro: `CSTR_BUFFER()`** ```c #define CSTR_BUFFER(var) \ char var##_cstring[CSTR_STACK_SIZE] = {0}; \ struct __cstr_data var##_cstr_data = {var##_cstring, 0, CSTR_ONSTACK, 0}; \ cstr_buffer var; \ var->str = &var##_cstr_data; ``` :::info Reference: C99 Standard - §6.10.3.3 The ## operator ::: 創造一個 `cstring` 指標結構的 buffer (`cstr_buffer`),其字串指標成員 `cstr` 指向從 stack 上抓取的大小為 `CSTR_STACK_SIZE * sizeof(char)` 的一段空間,`hash_size` (在此為 `size`) 以及 `ref` (reference counting) 此時皆設為 `0`,狀態設為 `ON_STACK`。 **Function-like macro: `CSTR_LITERAL()`** ```c #define CSTR_LITERAL(var, cstr) \ static cstring var = NULL; \ if (!var) { \ cstring tmp = cstr_clone("" cstr, (sizeof(cstr) / sizeof(char)) - 1); \ if (tmp->type == 0) { \ tmp->type = CSTR_PERMANENT; \ tmp->ref = 0; \ } \ if (!__sync_bool_compare_and_swap(&var, NULL, tmp)) { \ if (tmp->type == CSTR_PERMANENT) \ free(tmp); \ } \ } ``` 首先,被宣告的變數 `var` 為 static variable,擁有完整的程式執行生命週期 (life cycle)。 之後,被宣告的 `cstring` 變數 `tmp` 會接住 `cstr_clone()` 的回傳值,其中 `cstr_clone()` 會複製 `cstr` 字串並對此做 string interning,詳細情形會在 **`cstr.c`** 做說明。 而有關 `__sync_bool_compare_and_swap()` GNU C Extentions 的使用,則為考量到多執行緒的狀況下,因 `cstr_clone()` 執行過程中有機會動態配置記憶體,可能因此造成 memory leak 的問題。 :::info Reference: [GNU C extensions - §5.44 Built-in functions for atomic memory access](https://gcc.gnu.org/onlinedocs/gcc-4.1.1/gcc/Atomic-Builtins.html) ::: **Function-like macro: `CSTR_CLOSE()`** ```c #define CSTR_CLOSE(var) \ do { \ if (!(var)->str->type) \ cstr_release((var)->str); \ } while (0) ``` 被傳進來的參數 `var` 預期的值為指向結構體 `struct __cstr_buffer` 的指標。而 `do{ ... } while (0)` 會強制讓整個迴圈內的行為只留在迴圈內,並且只執行一次。 `cstr_release()` 會釋放掉在 `cstring` 結構內的成員變數 `cstr` 被動態配置的記憶體。 有關 `var->str->type` 的值為 `0` 的狀況以及 `cstr_release()` 的作用會在 `cstr.c` 詳細說明。 #### `cstr.c` **`struct`** ```c struct __cstr_node { char buffer[CSTR_INTERNING_SIZE]; struct __cstr_data str; struct __cstr_node *next; }; ``` * `buffer` - 存成員 `str` 內部的字串值的 buffer。 * `str` - `struct __cstr_data` 型別,內容包含字串值,記憶體存放區域及狀態,hash value or size,以及 reference counting。 * `next` - 指向下一個 `__cstr_node` 結構的指標。 ```c struct __cstr_pool { struct __cstr_node node[INTERNING_POOL_SIZE]; }; ``` * `node` - 為 `__cstr_node` 結構體的陣列,陣列大小為 `INTERNING_POOL_SIZE`。 此結構存在的目的是為了讓 `malloc()` 在動態配置記憶體時,能夠配置出一段連續的記憶體空間。讓在 string interning 的過程中,實現 cache 機制優化效能,也避免了過多次 `malloc()` 造成 [memory fragmentation](https://www.researchgate.net/publication/295010953_Dynamic_Memory_Allocation_and_Fragmentation) 的問題。 ```c struct __cstr_interning { int lock; int index; unsigned size; unsigned total; struct __cstr_node **hash; struct __cstr_pool *pool; }; ``` * `lock` - 考量多執行緒的情況,需要此成員變數來避免存取字串過程中造成錯誤。 * `index` - 存 `pool` 指向的陣列的某個 index。 * `size` - 存目前 hash table 的大小。 * `total` - 存目前被駐留的字串 (interned strings) 的數量。 * `hash` - 為一個指向 `struct __cstr_node` 結構體的指標的指標。在 string interning 過程中會連接 `pool` 裡面的 node 而逐漸成為一個以 `struct __cstr_node` 指標為單位所組成的 hash table。 * `pool` - 為一個指向 `struct __cstr_pool` 結構體的指標,其成員存著一塊連續的記憶體 (`node[INTERNING_POOL_SIZE]`)。而這塊連續的空間之後會在 string interning 的過程中,逐漸成為一個 hash table。 **global variable** ```c static struct __cstr_interning __cstr_ctx; ``` **函式: `xalloc()`** ```c static void *xalloc(size_t n) { void *m = malloc(n); if (!m) exit(-1); return m; } ``` 動態配置記憶體。 `xalloc()` 用 `if` 條件式包裝 `malloc()`,讓 `malloc()` 若在動態配置記憶體失敗回傳 `NULL` 時,直接終止整支程式。 **函式:`insert_node()`** ```c static inline void insert_node(struct __cstr_node **hash, int sz, struct __cstr_node *node) { uint32_t h = node->str.hash_size; int index = h & (sz - 1); node->next = hash[index]; hash[index] = node; } ``` 將 `node` 放進 hash table `hash` 裡面。 其中 `h & (sz - 1)` 可以視為是 `h % sz`,目的是為了確保 hash value 沒有越界存取記憶體,造成 segmentation fault。但是此舉也有可能造成不同字串有著相同的 hash value,發生碰撞。 **函式: `expand()`** ```c static void expand(struct __cstr_interning *si) { unsigned new_size = si->size * 2; if (new_size < HASH_START_SIZE) new_size = HASH_START_SIZE; struct __cstr_node **new_hash = xalloc(sizeof(struct __cstr_node *) * new_size); memset(new_hash, 0, sizeof(struct __cstr_node *) * new_size); for (unsigned i = 0; i < si->size; ++i) { struct __cstr_node *node = si->hash[i]; while (node) { struct __cstr_node *tmp = node->next; insert_node(new_hash, new_size, node); node = tmp; } } free(si->hash); si->hash = new_hash; si->size = new_size; } ``` 增加 hash table size,每次增加一倍。 注意新宣告的 `unsigned` type 的變數 `new_size`,有可能在增加一倍的時候 overflow,所以必須要加上這段程式碼: ```c if (new_size < HASH_START_SIZE) new_size = HASH_START_SIZE; ``` 在成功動態配置一段新的記憶體之後,將舊的 hash table 指向的每個 node 放進這塊新的空間裡面,再將舊的 hash table 佔的空間 `free()` 掉,然後再讓 `si->hash` 指向這段新的空間,成為一個新的 hash table。 **函式: `interning()`** ```c static cstring interning(struct __cstr_interning *si, const char *cstr, size_t sz, uint32_t hash) ``` 實現字串駐留 (string interning) 的實作。 ```c if (!si->hash) return NULL; ``` 當實現字串駐留 (string interning) 裡面的 hash table 為空,無法找到 interned strings,回傳 `NULL`。 ```c int index = (int) (hash & (si->size - 1)); struct __cstr_node *n = si->hash[index]; ``` `(int) (hash & (si->size - 1))` 可以視為是 `(int) (hash % si->size)`,此舉確保 hash value 不會超過 hash table 的 index 的最大值,同時確保與函式 `insert_node()` 在實際決定存取 hash table 的 index 值時,有著同樣的規則。 ```c while (n) { if (n->str.hash_size == hash) { if (!strcmp(n->str.cstr, cstr)) return &n->str; } n = n->next; } ``` `while()` 迴圈內的行為是,找到實際真正對應的 interned string。 由於先前在 `insert_node()` 的實作,發現有可能發生 hash value 的碰撞,故而發生碰撞的資料會存成一個 singly linked list 連接在該 hash table 對應的位置。因此需要迴圈內跑完整個 linked list 找出實際真正對應的 interned string。 若成功找到對應的 interned string,則回傳該 interned string 的記憶體參照 (reference)。 ```c // 80% (4/5) threshold if (si->total * 5 >= si->size * 4) return NULL; ``` 若先前沒有成功找到對應的 interned string,且 hash table 內節點密度太高,有可能發生 hash value 碰撞的次數太多(在這裡的情況是,interned string 的節點個數為 hash table 能容納的大小的 80%),有可能因此影響到 hash 的效率,所以不宜再增加 interned string 的節點數,於是回傳 `NULL`。 但是此方法無法保證 hash table 內,interned string 節點皆是散在 hash table 的各處,此判斷方式並不嚴謹。 ```c if (!si->pool) { si->pool = xalloc(sizeof(struct __cstr_pool)); si->index = 0; } n = &si->pool->node[si->index++]; memcpy(n->buffer, cstr, sz); n->buffer[sz] = 0; cstring cs = &n->str; cs->cstr = n->buffer; cs->hash_size = hash; cs->type = CSTR_INTERNING; cs->ref = 0; n->next = si->hash[index]; si->hash[index] = n; return cs; ``` 若先前沒有成功找到對應的 interned string,且 hash table 內的節點的密度沒有太大,則允許 hash table 加入更多的 interned string 節點。 首先確認可被拿來使用的空間 `pool` 是否為空,若為空,則動態配置記憶體並初始化其對應資料 (`si->index`)。之後拿這段空間,以`__cstr_node` 為單位,來當作即將放入 hash table 的節點。 由於先前已經有確認在 interned string 的節點個數為 hash table 大小的 80% 時會回傳 `NULL`,所以 `si->index++` 不會因此越界存取記憶體而造成 segmentation fault。 再加入 interned string 節點至 hash table 的過程中,首先先複製傳進來的字串參數至 buffer (`n->buffer`) 中,結尾要加上 null terminator `'\0'` (`0`),之後再將真正要存 interned string 的字串指標 (`n->str->cstr`) 指向該 buffer (`n->buffer`) 的位置,之後再 interned string 的節點結構的內部成員 `cs->type` 狀態賦值為 `CSTR_INTERNING`,同時因為是新增至 hash table 的節點,所以 reference count `cs->ref` 為 `0`。 最後才真正將該節點放入 hash table 內,讓該 interned string 節點指向 hash table 對應的位置。 **函式: `cstr_interning()`** ```c static cstring cstr_interning(const char *cstr, size_t sz, uint32_t hash) { cstring ret; CSTR_LOCK(); ret = interning(&__cstr_ctx, cstr, sz, hash); if (!ret) { expand(&__cstr_ctx); ret = interning(&__cstr_ctx, cstr, sz, hash); } ++__cstr_ctx.total; CSTR_UNLOCK(); return ret; } ``` 函式 `interning()` 的包裝以及,考量多執行緒環境下的字串駐留 (string interning) 實現。 當第一次實行字串駐留 (string interning) `interning()` 失敗,回傳 `NULL`,則呼叫 `expand()` 將 hash table 擴大一倍,再次實行字串駐留 (string interning) 呼叫 `interning()`。 :::danger 假使 total 變數的意義是記錄 interned strings 的個數,那麼這裡 `++__cstr_ctx.total` 便不應該出現在此 `cstr_interning()` 函式中,因為 `interning()` 函式有可能回傳的值是已經被駐留的字串 (interned strings) 在 hash table 裡的記憶體位置參照,而非新增至 hash table 裡面的 interned string 節點。 所以 `++__cstr_ctx.total` 這行 expression 應該要放在 `interning()` 函式中。 ```diff static cstring cstr_interning(const char *cstr, size_t sz, uint32_t hash) { ... - ++__cstr_ctx.total; ... } ``` ```diff static cstring interning(struct __cstr_interning *si, const char *cstr, size_t sz, uint32_t hash) { ... n->next = si->hash[index]; si->hash[index] = n; + ++__cstr_ctx.total; } ``` ::: **函式: `hash_blob()`** ```c static inline uint32_t hash_blob(const char *buffer, size_t len) { const uint8_t *ptr = (const uint8_t *) buffer; size_t h = len; size_t step = (len >> 5) + 1; for (size_t i = len; i >= step; i -= step) h = h ^ ((h << 5) + (h >> 2) + ptr[i - 1]); return h == 0 ? 1 : h; } ``` 此為打散資料分布至 hash table 的 hash function。 :::info TODO: explain this hash function. ::: **函式: `cstr_clone()`** ```c cstring cstr_clone(const char *cstr, size_t sz) { if (sz < CSTR_INTERNING_SIZE) return cstr_interning(cstr, sz, hash_blob(cstr, sz)); cstring p = xalloc(sizeof(struct __cstr_data) + sz + 1); if (!p) return NULL; void *ptr = (void *) (p + 1); p->cstr = ptr; p->type = 0; p->ref = 1; memcpy(ptr, cstr, sz); ((char *) ptr)[sz] = 0; p->hash_size = 0; return p; } ``` 傳入要被讀取字串的資料的副本,確認是否符合 interned string 的大小,再決定要不要呼叫 `cstr_interning()`。若傳入的字串沒有做字串駐留 (string interning),則動態配置記憶體,賦予其對應的 `cstring` 型別結構體,再回傳其地址。 :::info 這裡 `cstring p = xalloc(sizeof(struct __cstr_data) + sz + 1);` 可以保證記憶體連續,所以在存取此 `cstring` 型別比起額外 `malloc()` 或者 `strdup()` 動態配置記憶體給 `p->cstr`,可以增加效能。 ::: :::warning 因為已經有 `xalloc()` 包裝 `malloc()`,所以當 `malloc()` 回傳 `NULL` 時,就會直接終止程式,因此就不需要額外再關注 `p` 的值是否為 `NULL`。 ```diff cstring cstr_clone(const char *cstr, size_t sz) { ... cstring p = xalloc(sizeof(struct __cstr_data) + sz + 1); - if (!p) - return NULL; ... return p; } ``` 但是若再多執行緒的情形下是否需要此 `if` 條件,尚待釐清。 ```diff cstring cstr_clone(const char *cstr, size_t sz) { ... - void *ptr = (void *) (p + 1); + char *ptr = (char *) (p + 1); ... } ``` 此處的 `(void *)` 型別應改為 `(char *)`,但是其輸出卻與型別不同沒有什麼差別,且觀察 `p` 後面不管加何種數值,輸出皆為一致。 實驗: **`test.c`** ```c #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> typedef struct __cstr_data { char *cstr; uint32_t hash_size; uint16_t type; uint16_t ref; } * cstring; cstring cstr_clone(const char *cstr, size_t sz) { cstring p = malloc(sizeof(struct __cstr_data) + sz + 1); void *ptr = (void *) (p + 1); p->cstr = ptr; p->type = 1; p->ref = 2; memcpy(ptr, cstr, sz); ((char *) ptr)[sz] = 0; p->hash_size = 3; return p; } cstring cstr_clone2(const char *cstr, size_t sz) { cstring p = malloc(sizeof(struct __cstr_data) + sz + 1); char *ptr = (char *) (p + 9999); p->cstr = ptr; p->type = 1; p->ref = 2; memcpy(ptr, cstr, sz); ((char *) ptr)[sz] = 0; p->hash_size = 3; return p; } int main(int argc, char *argv[]) { char test[] = "hello world!"; int len = strlen(test); cstring p = cstr_clone(test, len); cstring p2 = cstr_clone(test, len); printf("%d %d %d %s\n", p->hash_size, p->ref, p->type, p->cstr); printf("%d %d %d %s\n", p2->hash_size, p2->ref, p2->type, p2->cstr); return 0; } ``` ``` $ gcc test.c $ ./a.out 3 2 1 hello world! 3 2 1 hello world! ``` 尚待釐清原因。 // 推測指向字串的指標可為非連續記憶體 實驗: printf("%p")... ::: **函式: `cstr_grab()`** ```c cstring cstr_grab(cstring s) { if (s->type & (CSTR_PERMANENT | CSTR_INTERNING)) return s; if (s->type == CSTR_ONSTACK) return cstr_clone(s->cstr, s->hash_size); if (s->ref == 0) s->type = CSTR_PERMANENT; else __sync_add_and_fetch(&s->ref, 1); return s; } ``` 此函式為抓取一個 `cstring` 型別,為指向 `struct __cstr_data` 結構體的指標,判斷其內部資料後,對其做相對應的處理。 檢查此結構體裡面的成員 `type`,分為以下的 case: * 若 `type` 是 `CSTR_PERMANENT` 或是 `CSTR_INTERNING`,意義上,為被動態配置記憶體但因字串大小過大而沒有做字串駐留 (string interning) 的字串,或是已經字串駐留的字串 (interned string),則回傳 `s` 指標本身。 * 若 `type` 是 `CSTR_ONSTACK`,則代表 `__cstr_data` 結構體本身是作為一個 buffer 存在,因此呼叫 `cstr_clone()` 並回傳其回傳的 `cstring` 指標。 * 當 `type` 為 `0` 時,且 `ref` (reference count) 為 `0` 時,將 `type` 賦值為 `CSTR_PERMANENT`。 * 當 `type` 為 `0` 時,且 `ref` (reference count) 不為 `0`,則將 `ref` 的值 + 1。 **函式: `cstr_release()`** ```c void cstr_release(cstring s) { if (s->type || !s->ref) return; if (__sync_sub_and_fetch(&s->ref, 1) == 0) free(s); } ``` 此函式會將符合條件的 `cstring` 指標指向的結構體,釋放其被動態配置的記憶體。 符合的條件如下: * 其成員變數 `type` 必須為 `0`。 * 其成員變數 `ref` (reference counting) 被 `-1` 後必須為 `0`。 若 `type` 為 `0` 但 `ref` (reference count) 不為 `0`,則將 `ref` 的值 `-1`。 :::warning 但上述程式碼產生一個問題:若傳進來的 `cstring s` 一開始的 `type` 為 `0` 且 `ref` 為 `0` 時,就會直接 `return`,但是若只觀察在 `quiz2` 的程式碼,直覺上不會發生這種情況,不知此設計基於何種考量?是否為多執行緒上的考量? ::: **函式: `cstr_hash()`** ```c static size_t cstr_hash(cstring s) { if (s->type == CSTR_ONSTACK) return hash_blob(s->cstr, s->hash_size); if (s->hash_size == 0) s->hash_size = hash_blob(s->cstr, strlen(s->cstr)); return s->hash_size; } ``` 對於傳進來 `cstring s`,判斷其內部成員的資料後,決定並回傳 `hash_size` 的值。 當傳進來的 `cstring s` 字串的狀態為 `CSTR_ONSTACK`,代表此結構體的 `hash_size` 的值為字串的長度 (`size`),而非 hash values (`hash`),因此呼叫 `hash_blob()` 計算並回傳其 hash value。 如果 `(hash_size == 0)` 成立,則代表傳進來的字串會是字串大小過大而沒有做字串駐留 (string interning) 的字串結構體。不會是作為 buffer 的結構體是因為 buffer 本身狀態是 `CSTR_ONSTACK`,所以會是上一個條件判斷的 case。,此情形亦會計算並回傳其 hash value。 **函式: `cstr_equal()`** ```c int cstr_equal(cstring a, cstring b) { if (a == b) return 1; if ((a->type == CSTR_INTERNING) && (b->type == CSTR_INTERNING)) return 0; if ((a->type == CSTR_ONSTACK) && (b->type == CSTR_ONSTACK)) { if (a->hash_size != b->hash_size) return 0; return memcmp(a->cstr, b->cstr, a->hash_size) == 0; } uint32_t hasha = cstr_hash(a); uint32_t hashb = cstr_hash(b); if (hasha != hashb) return 0; return !strcmp(a->cstr, b->cstr); } ``` 傳進來參數是 `cstring` 型別的 `a`、`b`,為指向 `struct __cstr_data` 結構體的指標,此函式會判斷兩個指標中,結構體內的字串是否相同。處理方式會分幾個 case,詳述如下: * 若傳進來的 `a`、`b` 指標為同一個指標,則 `return 1` 代表字串內容相同。 * 若傳進來的 `a`、`b` 皆為 interned string,則皆為 hash table 內的節點,因此字串內容不會是一樣的,所以 `return 0`。 * 若傳進來的 `a`、`b` 其結構體內所存的字串皆在 stack 上,`type ` 皆為 `CSTR_ONSTACK`,則兩個結構體內的 `hash_size` 紀錄的會是字串的長度 (`size`),而非 hash values (`hash`),因此若兩者的長度不同,便可以得知兩個字串的內容不相符。若兩者長度相同,則交由 `memcmp()` 逐 byte (`unsigned char`) 檢查兩者是否相同。 * 若傳進來的 `a`、`b` 皆不符合上述的 case,則交由 hash function 比對兩者 hash 值,若 hash 值不相同,則 `return 0`。 * 最後若上述的所有 case 沒有一個是符合的,才交由 `strcmp()` 比對字串並回傳結果。 :::info `memcmp()` vs `strcmp()` // TODO reference: [What is the difference between memcmp, strcmp and strncmp in C? from Stack Overflow](https://stackoverflow.com/questions/13095513/what-is-the-difference-between-memcmp-strcmp-and-strncmp-in-c) [memcmp versus strcmp, why is memcmp faster? from Tek-Tips.COM](https://www.tek-tips.com/viewthread.cfm?qid=1323083) ::: **函式: `cstr_cat2()`** ```c static cstring cstr_cat2(const char *a, const char *b) { size_t sa = strlen(a), sb = strlen(b); if (sa + sb < CSTR_INTERNING_SIZE) { char tmp[CSTR_INTERNING_SIZE]; memcpy(tmp, a, sa); memcpy(tmp + sa, b, sb); tmp[sa + sb] = 0; return cstr_interning(tmp, sa + sb, hash_blob(tmp, sa + sb)); } cstring p = xalloc(sizeof(struct __cstr_data) + sa + sb + 1); if (!p) return NULL; char *ptr = (char *) (p + 1); p->cstr = ptr; p->type = 0; p->ref = 1; memcpy(ptr, a, sa); memcpy(ptr + sa, b, sb); ptr[sa + sb] = 0; p->hash_size = 0; return p; } ``` // duplicate & concat strings, case for heap allocated strings. Also interns the concatenated string after duplicate & concat is done. **函式: `cstr_cat()`** ```c cstring cstr_cat(cstr_buffer sb, const char *str) { cstring s = sb->str; if (s->type == CSTR_ONSTACK) { int i = s->hash_size; while (i < CSTR_STACK_SIZE - 1) { s->cstr[i] = *str; if (*str == 0) return s; ++s->hash_size; ++str; ++i; } s->cstr[i] = 0; } cstring tmp = s; sb->str = cstr_cat2(tmp->cstr, str); cstr_release(tmp); return sb->str; } ``` // duplicate & concat strings, case for ONSTACK buffer. ### 對應測試程式 // TODO :::danger 還沒改之前,用 valgrind 測試是否有 memory leak ``` $ valgrind --leak-check=full ./cstr ==45589== Memcheck, a memory error detector ==45589== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al. ==45589== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info ==45589== Command: ./cstr ==45589== equal ==45589== ==45589== HEAP SUMMARY: ==45589== in use at exit: 57,472 bytes in 2 blocks ==45589== total heap usage: 3 allocs, 1 frees, 58,496 bytes allocated ==45589== ==45589== LEAK SUMMARY: ==45589== definitely lost: 0 bytes in 0 blocks ==45589== indirectly lost: 0 bytes in 0 blocks ==45589== possibly lost: 0 bytes in 0 blocks ==45589== still reachable: 57,472 bytes in 2 blocks ==45589== suppressed: 0 bytes in 0 blocks ==45589== Reachable blocks (those to which a pointer was found) are not shown. ==45589== To see them, rerun with: --leak-check=full --show-leak-kinds=all ==45589== ==45589== For lists of detected and suppressed errors, rerun with: -s ==45589== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) ``` 有 memory leak,要改進. TODO: rerun the valgrind for adding the option --leak-check=full --show-leak-kinds=all -s for tracing the memory leak function. ::: ### 考量多執行緒實現字串駐留 (String Interning) :::info TODO ::: ### 分析效能表現 :::info TODO :::

    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