# 2020q1 Homework3 (quiz3)
contributed by < `ambersun1234` >
## 開發環境
```shell
$ uname -a
Linux station 5.4.72-microsoft-standard-WSL2 #1 SMP Wed Oct 28 23:40:43 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
$ gcc -v
gcc version 9.3.0 (Ubuntu 9.3.0-17ubuntu1~20.04)
```
## 解釋程式運作原理
```c=
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, 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;
```
上述 union 大小總共 16 bytes,其中 data[16] 與 另外兩個 struct 共用這個 16 bytes;
根據講解,長度 $\leq 15$ 的字串會存放在 stack,長度 $\geq 16$ 的字串會放在 heap,但是我們可以很明顯地觀察到 data 總共擁有 16 bytes,多的那個 1 個 bytes 則是為了要讓 `space_left`, `is_ptr`, `flag1`, `flag2`, `flag3` 用的,而其中 filler[15] 是為了要對齊 data 前 15 bytes 所用,避免更改到資料部分
注意到 union 中 `bit field` 的用法,根據 [C 語言規格書 §6.7.2.1 - 8](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf#page=114)
> A member of a structure or union may have any object type other than a variably modified type.105) In addition, a member may be declared to consist of a specified number of bits (including a sign bit, if any). Such a member is called a bit-field; 106) its width is preceded by a colon.
bit field 可以定義該欄位需要多少 ==bits==,在空間利用上,這是一個很好的技巧
<hr>
```cpp
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 + OFF);
return (char *) x->ptr;
}
```
這個函式主要在取得對應的指標,分別有放在短字串的 `x->data`、中字串的 `x->ptr` 以及長字串的 `x->ptr + OFF`
根據註解 `/* The extra 4 bytes are used to store the reference count */` 我們可以得知 OFF 為 4 bytes
<hr>
```cpp
/* lowerbound (floor log2) */
static inline int ilog2(uint32_t n) { return LLL; }
```
取出小於等於 n 的 power of 2,考慮 $13_{(10)} = 0000 ..... 1101_{(2)}$
距離 13 最近的 power of 2 為 16($0000 .... 0001 0000_{(2)}$) 在位置 4 上面
因為 power of 2 在二進位的表示上,都只會有一個 bit
13 的二進位上,只有4個 bit 有值,而使用 [clz](https://en.wikipedia.org/wiki/Find_first_set#CLZ) 我們可以很輕易的取得前方有多少位元空著的,透過簡單的減法運算可以得到 4(32 - clz(n))
又因為這個函式是要取 lowerbound ,所以必須在 - 1
也可以用 [ctz](https://en.wikipedia.org/wiki/Find_first_set#CTZ) 進行改寫
變成
```cpp
/* lowerbound (floor log2) */
static inline int ilog2(uint32_t n) { return __builtin_ctz(n) - 1; }
```
<hr>
```cpp
xs *xs_new(xs *x, const void *p)
{
*x = xs_literal_empty();
size_t len = strlen(p) + 1;
if (len > NNN) {
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;
}
```
NNN 為 16,因為大於 16 就必定需要用到 heap 進行存取
<hr>
```cpp
#define check_bit(byte) (CCC)
#define set_bit(byte) (SSS)
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
```
該 function 為 trim,也就是去除特定字元,所以不難猜測到 check_bit 是用於檢查是否有一樣的字元,而 set_bit 則是將要去除字元寫入 mask
值得注意的是,解答給了一個我覺得很神奇的解法
```c
mask[(uint8_t) byte / 8] & 1 << (uint8_t) byte % 8
mask[(uint8_t) byte / 8] |= 1 << (uint8_t) byte % 8
```
根據 [ASCII](https://zh.wikipedia.org/wiki/ASCII) 的定義,它包含了 128 個字元 (可用 8 個 bits 表示)
解答採用的方式是,用一個 map 去對應,前 5 個 bits 是 index,後 3 個 bits 是 value
在 value 的方面,也不是簡單的儲存 bits,而是把它當作 `shift amount`
因為,可想而知,如果直接採用後 3 bits,一定會遇到 `000` 的情況,變成再比對的時候,條件會一直成立,即使沒有把該字元寫入 trimset (因為一開始就把 mask 初始化為 0)
也就是說,value 的值會從 2 (`1 << 1`) ~ 128 (`1 << 7`)
---
## 字串複製 (應考慮到 CoW) 的實作
```cpp
// parameter: src, des
static xs *xs_cow_copy(xs *des, xs *src) {
if (!xs_is_ptr(src)) {
*des = *src;
} else if (xs_is_large_string(src)) {
des->capacity = src->capacity;
des->is_ptr = src->is_ptr;
des->size = src->size;
xs_inc_refcnt(src);
des->ptr = xs_data(src);
} else {
des->capacity = src->capacity;
des->is_ptr = src->is_ptr;
des->size = src->size;
size_t len = xs_size(src) + 1;
xs_allocate_data(des, len, 0);
memcpy(xs_data(des), xs_data(src), len);
}
}
```
輸出如下
```
test cow copy on stack
[(((foobarbar)))] : 15
[(((foobarbar)))] : 15
[0x7fff06372490] : [0x7fff06372430]
test cow copy on heap
[abcdefghijklmnopqrstuv] : 22
[abcdefghijklmnopqrstuv] : 22
[0x5573b42c56e0] : [0x5573b42c56b0]
test cow copy on heap
[cHbC2VnYvV3O0MMwC1GEVfWQUHozZUkicxkxEgP2ySifc7P5edWzuyPiOoyxYAmVs5ExtHMDlPUYveflnJqF3b9X9qWtQmrsHuUOrnGfLLyrRM23t5SsnGTv7ArtseIG4N4NBSm632jQJTdu11lcGJQZj5mFnQE0Zcc78J9cazKYLF0AYe4bLiUTXJKsMKhxcvqsIVBQvpedDViocbVgcatIRsqnFUtywx5DLlHjcBNpMNXyAf47gXa4NEctfc7UioLx] : 260
[cHbC2VnYvV3O0MMwC1GEVfWQUHozZUkicxkxEgP2ySifc7P5edWzuyPiOoyxYAmVs5ExtHMDlPUYveflnJqF3b9X9qWtQmrsHuUOrnGfLLyrRM23t5SsnGTv7ArtseIG4N4NBSm632jQJTdu11lcGJQZj5mFnQE0Zcc78J9cazKYLF0AYe4bLiUTXJKsMKhxcvqsIVBQvpedDViocbVgcatIRsqnFUtywx5DLlHjcBNpMNXyAf47gXa4NEctfc7UioLx] : 260
[0x5573b42c5714] : [0x5573b42c5714]
reference count: 2
```
實作 [Copy on Write - CoW](https://en.wikipedia.org/wiki/Copy-on-write) 時要注意長字串還有 reference count 需要修正。
---
## 設計實驗,確認上述字串處理最佳化 (針對空間效率) 帶來的效益,應考慮到 [locality of reference](https://en.wikipedia.org/wiki/Locality_of_reference)
以時間效率來說,根據 [Stack vs Heap Memory Allocation](https://www.geeksforgeeks.org/stack-vs-heap-memory-allocation/) 中指出,stack memory allocation 中的操作由編譯器事先規劃,而且記憶體區段是連續的,也因此相較於 heap memory allocation 來說,存取速度會比 heap 還要快
空間效率的部分。C++ [std::string](https://www.cplusplus.com/reference/string/string/) 只能使用一半的大小,詳見以下實驗
參照 [Exploring std::string](https://shaharmike.com/cpp/std-string/) 做了一個實驗(實作程式碼: [linux2021q1 quiz3/stdstringTest.cpp](https://github.com/ambersun1234/linux2021q1_quiz3/blob/master/stdstringTest.cpp))
```cpp
#include <iostream>
#include <string>
#include <cstdlib>
void *operator new(std::size_t n) {
std::cout << "Allocating " << n << " bytes";
return malloc(n);
}
void operator delete(void *p) throw() {
free(p);
}
int main(int argc, const char *argv[]) {
std::cout << "sizeof(std::string): " << sizeof(std::string) << std::endl;
for (int i = 0; i < 32; i++) {
std::cout << i << ": " << std::string(i, '=') << std::endl;
}
return 0;
}
```
得到的測試結果如下
```
sizeof(std::string): 32
0:
1: =
2: ==
3: ===
4: ====
5: =====
6: ======
7: =======
8: ========
9: =========
10: ==========
11: ===========
12: ============
13: =============
14: ==============
15: ===============
16: Allocating 17 bytes================
17: Allocating 18 bytes=================
18: Allocating 19 bytes==================
19: Allocating 20 bytes===================
20: Allocating 21 bytes====================
21: Allocating 22 bytes=====================
22: Allocating 23 bytes======================
23: Allocating 24 bytes=======================
24: Allocating 25 bytes========================
25: Allocating 26 bytes=========================
26: Allocating 27 bytes==========================
27: Allocating 28 bytes===========================
28: Allocating 29 bytes============================
29: Allocating 30 bytes=============================
30: Allocating 31 bytes==============================
31: Allocating 32 bytes===============================
```
由上可得知,std::string 僅能使用一半的空間
考慮空間效率實驗,詳見 [linux2021q1 quiz3/spaceTest.cpp](https://github.com/ambersun1234/linuxkernel_internals/blob/master/2021q1_quiz3/spaceTest.cpp)
![](https://github.com/ambersun1234/linuxkernel_internals/blob/master/2021q1_quiz3/img/spaceTest300.png?raw=true)
> $\uparrow$ 針對字串大小為 $1 \to 300$ 的 massif 輸出
![](https://github.com/ambersun1234/linuxkernel_internals/blob/master/2021q1_quiz3/img/spaceTest500.png?raw=true)
> $\uparrow$ 針對字串大小為 $1 \to 500$ 的 massif 輸出
綠色的部分是 `xs` 本身的佔用空間,黃色的部分是 `std::string`,青色的部分是 `xs heap` 的部分
根據 $1 \to 300$ 的輸出 (上圖) 可以得知,xs 的空間隨著字串大小越來越大,直到 `43.0KB`(綠色部分),後來就換成青色的部分,也就是 heap (malloc) 的部分
接著考慮 $1 \to 500$ 的輸出 (下圖),可以發現,黃色的部分 (`std::string`) 相比青色 (`xs heap`),增長的更快,也就是說,xs 的實作相比 `std::string` 來說確實可以減少記憶體使用量
###### tags: `linux2021`