# 2021q1 quiz3 - SSO/CoW
contributed by < `yellow951321` >
###### tags: `linux2021`
## source
[quiz3](https://hackmd.io/@sysprog/ryCRU-Hmu)
## struct
仿造 [folly:fbstring](https://github.com/facebook/folly/blob/master/folly/docs/FBString.md) 的目標, `xs union` 為了更高效能的短字串處理,定義了 16 個位元組,應用和實作方法如下:
> 字串長度小於或等於 15個位元組,則放在 stack 。
> 字串長度大於 16 個位元組則放在 heap (使用 malloc 系統呼叫配置所需記憶體)
> 放在 heap 的字串,會針對大於 `LARGE_STRING_LEN` 的字串重複使用,同時將字串前 4 byte 用於 reference counting。
reference counting 的用意是為了要記錄是否還有指標在使用這個字串,假如在釋放記憶體時發現超過兩個以上的指標使用字串時。不需要釋放 heap 中的記憶體,只要更改 reference counting 即可。
```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
* much like fbstring:
* https://github.com/facebook/folly/blob/master/folly/docs/FBString.md
*/
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, is_large_string : 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;
```
前半段為子串長度小於 16 時所使用的 struct ,後半為資料放在 heap 時的 struct。
1. `filler` : 當字串小於 16 時,字串的儲存位置。
2. `space_left` : 儲存了短字串中,剩餘的空間。計算公式為`15 - sizeof(filler)`
3. `is_ptr` : 紀錄資料是否被儲存在, heap 中。
4. `is_large_string` : 紀錄資料是否長度大於 `LARGE_STRING_LEN`
```
#define MAX_STR_LEN_BITS (54)
#define MAX_STR_LEN ((1UL << MAX_STR_LEN_BITS) - 1)
#define LARGE_STRING_LEN 256
```
1. `MAX_STR_LEN_BITS (54)` : 定義了可用於用於儲存資料的最大 bits 數量。
2. `MAX_STR_LEN` : 根據 `MAX_STR_LEN_BITS` 定義出的可以儲存的字串最大長度,在這次的實作中為 $2^{53} - 1$
3. `LARGE_STRING_LEN` : 定義了判定為 `LARGE_STRING` 的最小字串長度。用於當資料儲存在 heap 時跟 reference counting 有關的一些操作。
此外, 根據 [RZHuangJef](https://hackmd.io/@Forward1105/2021q1Hw_quiz3) 提到出假說。由於我們是用 unino 去宣告字串。我們必須保證當作為短字串時的 flags 不會和長字串中的各個 bit-fields影響。需要確認確認宣告時的 bit-fields 和 最後實作出的 bit-fields 的順序是和預期的一樣的。
在 IOS/IEC 9899 中,針對 structure 以及 union 的定義
* ISO/IEC 9899:1999 [6.7.2.1] Structure and union specifiers
> An implementation may allocate any addressable storage unit large enough to hold a bit-field. If enough space remains, a bit-field that immediately follows another bit-field in a structure shall be packed into adjacent bits of the same unit. If insufficient space remains, whether a bit-field that does not fit is put into the next unit or overlaps adjacent units is implementation-defined. The order of allocation of bit-fields within a unit (high-order to low-order or low-order to high-order) is implementation-defined. The alignment of the addressable storage unit is unspecified.
C99 規格書有提到,連續的 bit-fields 在記憶體上也會以相鄰的方式連接。但是連接的順序(由低記憶體位置到高記憶體位置或者反之)是 implementation-defined 。因此 [RZHuangJef](https://hackmd.io/@Forward1105/2021q1Hw_quiz3) 設置了一個實驗去檢驗實際上的記憶體順序為何。我試著重現這一段實驗,結果如下。
### test.c
``` c=
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
struct {
uint32_t a : 4,
b : 8,
c : 20;
} ts;
void print_bits(uint8_t byte)
{
for (int8_t i = 7; i >= 0; i--)
printf("%d", byte & (1 << i) ? 1 : 0);
}
void print_layout(const void *ptr, size_t len)
{
uint8_t *p = (uint8_t *)ptr;
for (size_t i = 0; i < len; i++, p++) {
printf("%p: ", p);
print_bits(*p);
printf("\n");
}
}
int main()
{
printf("Default:\n");
print_layout(&ts, sizeof(ts));
// set ts.a
ts.a = 0xF;
printf("\nts.a is set\n");
print_layout(&ts, sizeof(ts));
// set ts.b
ts.b = 0xFF;
printf("\nts.a & ts.b are set\n");
print_layout(&ts, sizeof(ts));
return 0;
}
```
實驗環境以及編譯過程
``` shell
$ gcc --version | head -n 1
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
$ lscpu | head -n 3
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
$ gcc -std=c99 test.c -o test
```
這次實驗中所宣告的 struct 有三個 bit-fields a b c ,長度分別為 4 8 20 。 實驗目的是要確認宣告的順序以及實際上實做出的順序的差異,值結果如下。
```shell=
Default:
0x562efb93a014: 00000000
0x562efb93a015: 00000000
0x562efb93a016: 00000000
0x562efb93a017: 00000000
ts.a is set
0x562efb93a014: 00001111
0x562efb93a015: 00000000
0x562efb93a016: 00000000
0x562efb93a017: 00000000
ts.a & ts.b are set
0x562efb93a014: 11111111
0x562efb93a015: 00001111
0x562efb93a016: 00000000
0x562efb93a017: 00000000
```
通過實驗,我們可以確定。連續的 bit-fields 是從低記憶體位置開始往高記憶體位置排序。
總之,這張圖表可以表示出各種情況下 `xs` 的記憶體配置情況
```
_______________________________________________________________________
|_____________________________data[16]__________________________________|
|____________________________filler[15]____________________|_sl_|_|_|_|_|
|______________ptr_______________|_________size________|capacity|\flags/
sl stands for space_left
```
## functions
### xs_is_ptr
```c=
static inline bool xs_is_ptr(const xs *x) { return x->is_ptr; }
```
回傳 xs 是否儲存在 heap 上。
### xs_is_large_string
``` c=
static inline bool xs_is_large_string(const xs *x)
{
return x->is_large_string;
}
```
回傳 xs 是否為 large string 。
### xs_size
```c=
static inline size_t xs_size(const xs *x)
{
return xs_is_ptr(x) ? x->size : 15 - x->space_left;
}
```
以 space_left 計算出 xs 目前的短字串長度。
### xs_data
```c=
static inline char *xs_data(const xs *x)
{
if (!xs_is_ptr(x))
return (char *) x->data;
if (xs_is_large_string(x))
return (char *) (x->ptr + 4);
return (char *) x->ptr;
}
```
回傳 xs 目前儲存的字串值,短字串直接回傳 data 即可。自串長度大於 15 但不符合 large string 的字串則直接回傳 ptr 中儲存的字串。而 large string 則需要額外 4 bytes 的 offset 為 reference counting 所用,詳細則在 `static void xs_allocate_data(xs *x, size_t len, bool reallocate)` 中有提到(參考 [gyes00205](https://hackmd.io/@gyes00205/2021q1_quiz3) 的共筆)
### xs_capacity
```c=
static inline size_t xs_capacity(const xs *x)
{
return xs_is_ptr(x) ? ((size_t) 1 << x->capacity) - 1 : 15;
}
```
計算字串的最大長度。字串長度小於 16 時為 on stack 字串,最大長度皆為 15。中長字串則需要藉由 capacity 來計算出可容納的最長字串大小。 capacity 儲存的是可以用來儲存字串的 bits 數, 因此需要用 $2^{capacity} - 1$ 去計算出真正的最大字串長度。
### xs_set_refcnt
```c=
static inline void xs_set_refcnt(const xs *x, int val)
{
*((int *) ((size_t) x->ptr)) = val;
}
```
當 xs 字串為 latge string 時, ptr 的前 4 byte 被使用在 reference counting 上。
根據 c99 規格書,對於 size_t 的定義為 unsigned interger。
* ISO/IEC 9899:1999 [7.17] Common definitions <stddef.h>
> which is the unsigned integer type of the result of the sizeof operator
以及 interger 和 unsigned interger 轉型有關的敘述
* ISO/IEC 9899:1999 [6.3.1.1] Boolean, characters, and integers
> An object or expression with an integer type whose integer conversion rank is less
than or equal to the rank of int and unsigned int.
可以確定 size_t 的長度為 4 bytes , 因此 `(size_t) x->ptr)` 的確只會拿取前 4 bytes 的數值取出。並且在沒有超出 signed interger 的數值範圍的前提下,可以安全強制轉型成 unsigned intergear 。
### xs_inc_refcnt
```c=
static inline void xs_inc_refcnt(const xs *x)
{
if (xs_is_large_string(x))
++(*(int *) ((size_t) x->ptr));
}
```
當 xs string 為 large string 時,對 reference counting 增加 1 。和 `xs_set_refcnt` 時有一樣的問題,假如造成 signed interger overflow 時,數值會出錯。
### xs_dec_refcnt
```c=
static inline int xs_dec_refcnt(const xs *x)
{
if (!xs_is_large_string(x))
return 0;
return --(*(int *) ((size_t) x->ptr));
}
```
做和 `static inline void xs_inc_refcnt(const xs *x)` 相對的事情,減少 reference counting 的數值。
### xs_get_refcnt
```c=
static inline int xs_get_refcnt(const xs *x)
{
if (!xs_is_large_string(x))
return 0;
return *(int *) ((size_t) x->ptr);
}
```
回傳 xs 的 reference counting
### xs_literal_empty
```c=
#define xs_literal_empty() \
(xs) { .space_left = 15 }
```
用於初始化出一個字串長度為 0 的 xs string。
### ilog2
```c=
static inline int ilog2(uint32_t n)
{
return 32 - __builtin_clz(n) - 1;
}
```
取 $log_2 n$ 的 lower bound 。
用 interger 的 bit 去減內建的 `__builtin_clz()` 計算出 leading zero 數後,再減去 1 (lower bound) 即可得到目標值。
以 8 bits 數為例
```
00111111
```
count leading zero 的值為 2, $2^{8-2-1}$ 即為 `00100000` ,因此 $8-2-1$ ,即為我們所要求的 ilog2 的值。
### xs_allocate_data
```c=
static void xs_allocate_data(xs *x, size_t len, bool reallocate)
{
/* Medium string */
if (len < LARGE_STRING_LEN) {
x->ptr = reallocate ? realloc(x->ptr, (size_t) 1 << x->capacity)
: malloc((size_t) 1 << x->capacity);
return;
}
/* Large string */
x->is_large_string = 1;
/* The extra 4 bytes are used to store the reference count */
x->ptr = reallocate ? realloc(x->ptr, (size_t)(1 << x->capacity) + 4)
: malloc((size_t)(1 << x->capacity) + 4);
xs_set_refcnt(x, 1);
}
```
初始化中長字串的 xs 的字串內容。並且假如為 large string 則同時將 reference counting 設為 1 。
### xs_new
```c=
xs *xs_new(xs *x, const void *p)
{
*x = xs_literal_empty();
size_t len = strlen(p) + 1;
if (len > 16) {
x->capacity = ilog2(len) + 1;
x->size = len - 1;
x->is_ptr = true;
xs_allocate_data(x, x->size, 0);
memcpy(xs_data(x), p, len);
} else {
memcpy(x->data, p, len);
x->space_left = 15 - (len - 1);
}
return x;
}
```
建立一個新的 xs 字串,當字串長度大於短字串的最大長度時。則需要額外配置記憶體空間。
要注意的是 `size_t len = strlen(p) + 1;` 有多加上 1 補上 null terminator 所占用的空間。因此下一行的 `if (len > 16)` 是大於 16 而非原本 xs filler 的長度 15 。
### xs_tmp
```c=
#define xs_tmp(x) \
((void) ((struct { \
_Static_assert(sizeof(x) <= MAX_STR_LEN, "it is too big"); \
int dummy; \
}){1}), \
xs_new(&xs_literal_empty(), x))
```
回傳一個新的 xs 字串副本,並且利用 `_Static_assert` 檢查 string x 的自串長度有沒有超過 MAX_STR_LEN。
### xs_grow
```c=
/* grow up to specified size */
xs *xs_grow(xs *x, size_t len)
{
char buf[16];
if (len <= xs_capacity(x))
return x;
/* Backup first */
if (!xs_is_ptr(x))
memcpy(buf, x->data, 16);
x->is_ptr = true;
x->capacity = ilog2(len) + 1;
if (xs_is_ptr(x)) {
xs_allocate_data(x, len, 1);
} else {
xs_allocate_data(x, len, 0);
memcpy(xs_data(x), buf, 16);
}
return x;
}
```
增加 xs 字串的最大字串長度,假如字串長度足夠則直接回傳。假如自串長度不夠時,需要檢查為長字串或者短字串。但在第 13 行, `x->is_ptr = true;` 。卻直接接 x 設定為長字串。會造成 x 原本假如是短字串時,無法執行到 19、20 行。造成短字串的字串內容遺失。因此,根據 [gyes00205](https://hackmd.io/@gyes00205/2021q1_quiz3) ,以及 [ccs100203](https://hackmd.io/@cccccs100203/linux2021-quiz3) 討論出的方法將 `x->is_ptr = true;` 移至 if 下方。
```c=
xs *xs_grow(xs *x, size_t len)
{
char buf[16];
if (len <= xs_capacity(x))
return x;
/* Backup first */
if (!xs_is_ptr(x))
memcpy(buf, x->data, 16);
x->capacity = ilog2(len) + 1;
if (xs_is_ptr(x)) {
xs_allocate_data(x, len, 1);
} else {
xs_allocate_data(x, len, 0);
memcpy(xs_data(x), buf, 16);
}
x->is_ptr = true;
return x;
}
```
### xs_newempty
```c=
static inline xs *xs_newempty(xs *x)
{
*x = xs_literal_empty();
return x;
}
```
將 x 設為一個初始化的 xs 短字串。
### xs_free
```
static inline xs *xs_free(xs *x)
{
if (xs_is_ptr(x) && xs_dec_refcnt(x) <= 0)
free(x->ptr);
return xs_newempty(x);
}
```
初始化 x 的字串內容。假如為長字串時,先檢查 refrence counting 是否大於 0 ,假如大於 0 則代表仍然有其他的 xs 正在使用這個字串,因此不能釋放記憶體。假如為 0 則釋放記憶體空間。
### xs_cow_lazy_copy
```c=
static bool xs_cow_lazy_copy(xs *x, char **data)
{
if (xs_get_refcnt(x) <= 1)
return false;
/* Lazy copy */
xs_dec_refcnt(x);
xs_allocate_data(x, x->size, 0);
if (data) {
memcpy(xs_data(x), *data, x->size);
/* Update the newly allocated pointer */
*data = xs_data(x);
}
return true;
}
```
為 reference counting 大於 1 的 `xs->ptr` 建立一個新的記憶體空間並且將字串值複製過去。
```graphviz
digraph {
rankdir=LR
block2 [shape="record", label="{ptr}"];
block1 [shape="record", label="{xs}"];
block [shape="record", label="{other}"];
block -> block2;
block1 -> block2;
}
```
```graphviz
digraph {
rankdir=LR
block3 [shape="record", label="{data}"]
block2 [shape="record", label="{ptr}"];
block1 [shape="record", label="{xs}"];
block [shape="record", label="{other}"];
block -> block2;
block1 -> block3;
}
```
### xs_concat
```c=
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);
xs_cow_lazy_copy(string, &data);
if (size + pres + sufs <= capacity) {
memmove(data + pres, data, size);
memcpy(data, pre, pres);
memcpy(data + pres + size, suf, sufs + 1);
if (xs_is_ptr(string))
string->size = size + pres + sufs;
else
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;
string->size = size + pres + sufs;
}
return string;
}
```
按照順序連接三個字串 prefix, string 和 suffix。假如字串 string 的最大字串長度能夠容納三個字串的總長度。則將字串內容複製到 string 上回傳即可。假如字串 string 的最大自串長度不夠,則利用 xs_grow 增加 string 的最大長度後再複製到 string 上。
### xs_trim
```c=
xs *xs_trim(xs *x, const char *trimset)
{
if (!trimset[0])
return x;
char *dataptr = xs_data(x), *orig = dataptr;
if (xs_cow_lazy_copy(x, &dataptr))
orig = dataptr;
/* similar to strspn/strpbrk but it operates on binary data */
uint8_t mask[32] = {0};
#define check_bit(byte) (mask[(uint8_t) byte / 8] & 1 << (uint8_t) byte % 8)
#define set_bit(byte) (mask[(uint8_t) byte / 8] |= 1 << (uint8_t) byte % 8)
size_t i, slen = xs_size(x), trimlen = strlen(trimset);
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;
/* reserved space as a buffer on the heap.
* Do not reallocate immediately. Instead, reuse it as possible.
* Do not shrink to in place if < 16 bytes.
*/
memmove(orig, dataptr, slen);
/* do not dirty memory unless it is needed */
if (orig[slen])
orig[slen] = 0;
if (xs_is_ptr(x))
x->size = slen;
else
x->space_left = 15 - slen;
return x;
#undef check_bit
#undef set_bit
}
```
用於根據 trimset 所給的字元來切割 xs string 的字串內容。
首先利用 `xs_cow_lazy_copy` 來建立一個新的字串,用於切割字串。
由於函式支援自訂欲剪除的字元,假如想排除的字元太多,逐一比對會造成效能低落。因此在這邊我們利用查表的方式來確認是否移除字元
(參考[課堂問答](https://hackmd.io/@sysprog/rk9fG7zG_)。
程式碼中,我們宣告一個長度為 32 的 uint8_t 型態的 mask 陣列。由於 $log_x{32}$ 為 5 , 並且 uint8_t 的大小為 8 bits , 且 $log_x{8}$ 為 3 。我們可以把一個欲查詢的字元分成前 5 bits 和後 3bits 。分別對應到 mask 的 index 以及 value 上。這樣即可利用 set_bit 以及 check_bit 兩個 macro 上快速查詢當前字元是否屬於 trimset 。舉例來說 `A` 的 ascii 碼為 '0x41' 。則 index 值為 8 中的第 2 個字元 ,也就是將 `mask[8]` 的第 2 個字元設為 1 。而 check_bit 只需要快速查表即可知道是否屬於 trimset。