# 2020q1 Homework2 (quiz2) contributed by < `OscarShiang` > ###### tags: `linux2020` ## 測試環境 ```shell $ cat /etc/os-release NAME="Ubuntu" VERSION="18.04.4 LTS (Bionic Beaver)" ID=ubuntu ID_LIKE=debian PRETTY_NAME="Ubuntu 18.04.4 LTS" VERSION_ID="18.04" HOME_URL="https://www.ubuntu.com/" SUPPORT_URL="https://help.ubuntu.com/" BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" VERSION_CODENAME=bionic UBUNTU_CODENAME=bionic $ cat /proc/version Linux version 5.3.0-40-generic (buildd@lcy01-amd64-024) (gcc version 7.4.0 (Ubuntu 7.4.0-1ubuntu1~18.04.1)) #32~18.04.1-Ubuntu SMP Mon Feb 3 14:05:59 UTC 2020 ``` ## 作業要求 - [x] 重新回答第 2 周測驗題 - [x] 解釋程式碼運作原理,並提出改進方案,可善用 GDB 追蹤和分析; - [x] 以上述程式碼為基礎,提供字串複製 (應考慮到 CoW) 和類似 strtok 功能的函式實作; - [ ] 設計實驗,確認上述字串處理最佳化 (針對空間效率) 帶來的效益,應考慮到 locality of reference; - [ ] 在 Linux 核心原始程式碼中,找出類似手法的 SSO 或 CoW 案例並解說; ## 測驗題分析 本題所在的位置是在 `xs.c:xs_new` 函式中 ```cpp xs *xs_new(xs *x, const void *p) { *x = xs_literal_empty(); size_t len = strlen(p) + 1; if (len > AAA) { x->capacity = ilog2(len) + 1; x->size = len - 1; x->is_ptr = true; x->ptr = malloc((size_t) 1 << x->capacity); memcpy(x->ptr, p, len); } else { memcpy(x->data, p, len); x->space_left = 15 - (len - 1); } return x; } ``` 這個函式的用途是初始化字串結構 `xs` 的相關設定,觀察 `xs` 的結構可以發現 ```cpp typedef union { /* allow strings up to 15 bytes to stay on the stack * use the last byte as a null terminator and to store flags */ char data[16]; struct { uint8_t filler[15], /* how many free bytes in this stack allocated string * same idea as fbstring */ space_left : 4, /* if it is on heap, set to 1 */ is_ptr : 1, flag1 : 1, flag2 : 1, flag3 : 1; }; /* heap allocated */ struct { char *ptr; /* supports strings up to 2^54 - 1 bytes */ size_t size : 54, /* capacity is always a power of 2 (unsigned)-1 */ capacity : 6; /* the last 4 bits are important flags */ }; } xs; ``` 可以看到 `len` 的大小就是 `p` 指標所指向的字元陣列包含空字元的大小,而在 `xs_new:11` 的地方就是將字串複製到 `data` 中 但是因為 `data` 的總長度只有 16 個 byte ,所以可以判斷 `xs_new:5` 的用意是判斷該字串 `p` 是否有超出 `data` 可以儲存的長度,所以正確答案就是: > AAA = 16 這題是在 `xs.c:xs_trim` 的位置,因為在這個函式中 `mask` 的用途是將 `trimset` 中的字元記錄在 `mask` 中 觀察 `xs_trim` 中的 macro ```cpp #define check_bit(byte) (mask[(uint8_t) byte / 8] CCC 1 << (uint8_t) byte % 8) #define set_bit(byte) (mask[(uint8_t) byte / 8] |= 1 << (uint8_t) byte % 8) ``` 可以發現 `set_bit` 可以把字元的數值,記錄在 `mask` 的其中一個位元中,所以考慮所有字元都可以存入的需求,也就是 [ACSII](http://www.asciitable.com) 所規範的 255 的字元都可以利用 `mask` 記錄下來的情況, `BBB` 應該為 32 ,也就是 $32 \times 8 = 256$ 個 bit 才能符合要求 > BBB = 32 根據 `xs_trim` 中對於 `check_bit` 的用途 ```cpp for (i = 0; i < trimlen; i++) set_bit(trimset[i]); for (i = 0; i < slen; i++) if (!check_bit(dataptr[i])) break; for (; slen > 0; slen--) if (!check_bit(dataptr[slen - 1])) break; dataptr += i; slen -= i; ``` 也就是確認該字元是否是要修剪的字元,所以應該算出來的數值應該是 - 非 `0` : 該字元是 `trimset` 中所出現的字元 - `0` : 該字元不是 `trimset` 中所出現的字元 而因為該字元所對應到 `mask` 的位置就是 `1 << (uint8_t) byte % 8)` 的位置,所以答案就是: > CCC = `&` ## 解釋程式運作原理 ### xs_tmp 根據 macro 的定義, `xs_tmp` 的用途就是回傳一個已經透過 `xs_new` 初始化的字串 ```cpp #define xs_tmp(x) \ ((void) ((struct { \ _Static_assert(sizeof(x) <= 16, "it is too big"); \ int dummy; \ }){1}), \ xs_new(&xs_literal_empty(), "" x)) ``` 因為在 `main` 函式中的呼叫是不是存進 pointer 中,而在 `xs_tmp` 最後所呼叫的 `*xs_new` 的回傳型態是卻是 pointer ,所以需要透過 derefernce 取得 :::warning 思考上述程式碼列表的 `16` 這個數值,解釋其作用,考慮到 cache,提出調整方案 :notes: jserv ::: > 好的,正在研讀 [Cache 原理和實際影響](https://hackmd.io/@n5i0JrAZRlOytHSWjelFMA/SkDKqf7b4/https%3A%2F%2Fhackmd.io%2Fs%2FHkyscQn2z?type=book),並思考改進的空間 > [name= Oscar][color= orange] ### xs_size 在 xs 的結構中,如果使用 stack 來儲存字串的話就會利用 space_left 來記錄剩餘的 byte 數,所以如果在 xstring 還是使用 stack 來記錄資料就可以利用 `16 - space_left` 取得字串長度,但是如果 stack 剛好用完,也就是 `'\0'` 位於第 16 個 byte 上也不會發生問題,因為 `'\0'` 在 ASCII 中對應到的就是數值上的 `0` 所以即使因為 `'\0'` 位在第 16 個 byte 而覆蓋到 `space_left` 的資料, `xs_size` 還是能正確地計算出字串的長度 ## 改進 xs_concat 在 `xs_concat` 實作中如果遇到連接的結果超出 `string` 的 stack 所能管理的長度時,會透過宣告一個新的字串 `tmp` 來連接字串,最後將原本的字串 `string` 釋放掉,換成 `tmp` 的字串 但是因為我們可以透過 `xs_grow` 調整字串的容量,所以就不需要另外宣告新的字串來處理連接,所以我透過 `xs_grow` 調整 `string` 的長度接著將 data 指標更新,最後再將字串複製到 `string` 裡面 ```diff xs *xs_concat(xs *string, const xs *prefix, const xs *suffix) { size_t pres = xs_size(prefix), sufs = xs_size(suffix), size = xs_size(string), capacity = xs_capacity(string); char *pre = xs_data(prefix), *suf = xs_data(suffix), *data = xs_data(string); if (size + pres + sufs <= capacity) { memmove(data + pres, data, size); memcpy(data, pre, pres); memcpy(data + pres + size, suf, sufs + 1); string->space_left = 15 - (size + pres + sufs); } else { - xs tmps = xs_literal_empty(); - xs_grow(&tmps, size + pres + sufs); - char *tmpdata = xs_data(&tmps); - memcpy(tmpdata + pres, data, size); - memcpy(tmpdata, pre, pres); - memcpy(tmpdata + pres + size, suf, sufs + 1); - xs_free(string); - *string = tmps; + xs_grow(string, size + pres + sufs); + data = xs_data(string); + memmove(data + pres, data, size); + memcpy(data, pre, pres); + memcpy(data + pres + size, suf, sufs + 1); + string->size = size + pres + sufs; } return string; } ``` 為了確定修改過後的版本能夠減少執行時間,我利用開機參數 `isolcpus` 將 CPU0 排除在開機程序外,接著透過 `taskset` 將程序綁定在 CPU0 上,並重複測量 $10^4$ 次,以 95% 的區間進行平均 而實驗結果發現修改過後的執行時間較原先的版本短,如下圖所示 ![](https://i.imgur.com/y5s4W7z.png) 因為省去 `malloc` 新空間與釋放 `string` 的程序,所以執行的效率較原先的版本好 ## 使用 Valgrind 檢查記憶體使用 利用 valgrind 檢查 memory leak 時發現,在字串使用 heap 來儲存時,會因為在 `main` 函式中沒有被釋放而導致 definitely lost ,所以必須在 `main` 函式使用完 xs 後強制進行 `xs_free` ,如果該 `xs` 是使用 heap 來儲存的話才能將 malloc 的空間歸還給記憶體 為了能在程式結束時自動執行 `xs_free` 函式釋放透過 heap 存取的字串,我在 xs 變數宣告的時候加入 gcc attribute `__cleanup__` 綁定 `xs_free` 函式,讓其在程式結束時會被呼叫,釋放記憶體空間 ```cpp #define autofree __attribute__((cleanup(xs_free))) autofree xs string; ``` ## 設計字串複製與 strtok 函式 因為需要考慮到 Copy-on-Write (CoW) 的議題,所以將字串複製分成兩種情況考慮 - 字串長度小於 16 ,直接將字串複製到 stack - 字串長度大於 16 ,透過 CoW 將 `char *ptr` 指向來源字串 ```cpp xs *xs_copy(xs *dst, xs *src) { if (xs_is_ptr(dst)) *dst = *xs_free(dst); if (xs_size(src) <= 16) { memcpy(dst, src, xs_size(src)); dst->is_ptr = 0; dst->space_left = src->space_left; } else { dst->ptr = src->ptr; dst->is_ptr = 1; dst->size = xs_size(src); dst->flag1 = 1; // indicate that the string is just a reference } return dst; } ``` 顧及 CoW,其他操作字串的函式也需修改 針對因為改寫來源字串而需要複製字串的函式定義新的函式 `xs_dup` 來簡化流程 ```cpp static xs *xs_dup(xs *dst) { int len = xs_size(dst); char *srcptr = xs_data(dst); *dst = *xs_newempty(dst); char *dataptr = xs_data(dst); memcpy(dataptr, srcptr, len); return dst; } ``` 因為如果 `dst` 是指標的話,我們可以先從 `xs_data` 函式取得 `dst` 指標對應的字串位置,並利用 `xs_size` 函式取得字串長度,當取得字串位置與字串長度之後,將 `dst` 字串重置,並將字串複製到 `dst` 中,最後回傳 `dst` 位置,完成複製 接著建構 `xs_is_ref` 函式讓我們可以知道該字串是否為 reference ,並將會操作到字串內容的函式: `xs_concat` 與 `xs_trim` 函式加入以下操作,在操作字串為參考的情況下複製字串以供操作 ```cpp if (xs_is_ref(string)) *string = *xs_dup(string); ``` 接著著手建構 `xs_tok` 函式 根據 `Linux Programmer's Manual` 所描述的 `strtok` : > The strtok() function is used to isolate sequential tokens in a null-terminated string, str. > These tokens are separated in the string **by at least one of the characters in sep**. > The first time that strtok() is called, str should be specified; subsequent calls, wishing to obtain further tokens from the same string, should **pass a null pointer** instead. The separator string, sep, **must be supplied each time**, and may change between calls. 因為在 `xs_tok` 中也要使用在 `xs_trim` 所使用到的 `mask` 的技巧,所以將 `#undef` 的巨集移動到 `xs_tok` 的結尾處 利用 for 迴圈判斷所在字元是否是 `sep` 集合中的其中一種,如果是的話,就將該字元改為 `\0` ,用指標的指標更新 `main` 函式內的字元指標 `last` 後,回傳字串開頭的指標完成 `xs_tok` ```cpp char *xs_tok(xs *str, const char *sep, char **last) { char *dataptr; if (str) { if (xs_is_ref(str)) *str = *xs_dup(str); dataptr = xs_data(str); } else dataptr = *last; if (!*dataptr) return NULL; uint8_t mask[32] = {0}; size_t i, len = strlen(dataptr), seplen = strlen(sep); for (i = 0; i < seplen; i++) set_bit(sep[i]); for (*last = dataptr; **last; ++(*last)) { if (check_bit(**last)) { **last = '\0'; ++(*last); break; } } return dataptr; #undef check_bit #undef set_bit } ``` :::danger 參考 `strtok_r` 來改寫,避免用到 `static char *dataptr` :notes: jserv ::: > 已參考 `strtok_r` 重新實作 `xs_tok` > [name= Oscar][color= orange] 而在 `xs_tok` 的實作中因為其也會改動到字串的資料,所以在開頭需要加上判斷其為字串參考的操作,避免複寫字串原先的資料 ## 參考資料 1. [2020q1 第 2 週測驗題](https://hackmd.io/@sysprog/linux2020-quiz2) 2. [Copy-on-write - Wikipedia](https://en.wikipedia.org/wiki/Copy-on-write) 3. [你所不知道的 C 語言:技巧篇](https://hackmd.io/@sysprog/c-trick) 4. [Using the `__cleanup__` variable attribute in GCC](https://echorand.me/site/notes/articles/c_cleanup/cleanup_attribute_c.html)