# 2020q1 Homework3 (quiz4)
contributed by < `Hsuhaoxiang` >
> [第 4 週測驗題](https://hackmd.io/@sysprog/linux2020-quiz4)
## 測驗 `1`
### 程式運作原理
* 程式的 prototype
* `bitcpy` 這個函式在進行位元的 copy
* `_dest` 欲寫入的 buffer 的 address
* `_write` 開始寫入的 offset
* `_src` 要讀取的 buffer 的 address
* `_read` 開始讀取的 offset
* `_count` 讀取多少個位元
```cpp
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)
```
---
* 讀取 _src buffer data
* 在 while 迴圈中做了兩件事:
* 1.讀取 `_src` 中的位元
* 2.寫入 `_dest`
* 因為 `data` 這個變數宣告成 `uint8_t` 只有8個 bits 所以 `bitsize` 不能直接設成 `_count` 大於8個 bits 就必須等到下一次 `while` 才能讀取及寫入,也間接回答出 ==KK2== 為 `bitsize` ( `_count -= bitsize` )
* 接著要從第 `_read + 1` 個 bit 開始讀取資料所以要先向左位移 `read_lhs`
* read_lhs 為 `_read & 7` 為什麼是 7 ? 因為要只需要左移0~7個 bits ,也可以看成是在做 mod8 ( _read %= 8)運算 ,如果 `_read` 這個 offset 大於7那我們要讀取的地方將會是 `*_src + (_read /8)` 然後從這個byte 開始,與 `write_lhs` 相似我們要寫入的地方也會是 `write_lhs = _write & 7` ,==KK1== 為 7。
* `read_rhs = 8 - read_lhs` 代表的這個 byte 還剩幾個 bits 可以讀取,因為前面已經經過 left shift 所以右邊會自動補零,只有 `read_lhs` 個 bits 是我們能讀的
* `if (bitsize > read_rhs)` 這個判斷在於如果需要取的 bits 數超過 `read_rhs` ,就必須讀取到下個 byte 的資料。
* 因為在`source` 將資料寫入 `data` 後已經經過 source++的處理已經是下個 byte 的資料,所以只要將他向右位移 `read_rhs` 再與 `data` 做 `or` 就能把下個 byte 的資料接在目前讀取的 data 後面
* 最後資料我們都將資料處理好了所以只要根據我們需要讀取的 bits 數找到對應的 mask 就能只讀出我們需要的 bits 。
```cpp
data = *source++;
bitsize = (_count > 8) ? 8 : _count;
if (read_lhs > 0) {
data <<= read_lhs;
if (bitsize > read_rhs)
data |= (*source >> read_rhs);
}
if (bitsize < 8)
data &= read_mask[bitsize];
```
---
* 寫入 _dest buffer
* `if (write_lhs > 0)` 看懂上面的code之後,可以猜想出這個判斷式就是看須不需要調整寫入 buffer 的位址,如果 `write_lhs` 為零的話自然不需要左右移
* `mask = read_mask[write_lhs]` 不需要寫入的方必須維持原來的值因此 從第0個 bit 到第 `write_lhs` 就需要靠 `read_mask` 將他們以外的 bit 清成0,再來需要將 `data` 寫入第 `write_lhs+1` 把 `data` 向右位移在與前面處理好的 `original & mask` 做 or 運算就能把最終要寫入這個 byte 的資料接在一起,如果要寫入的 bits 數大於這個 byte 中需要被寫入的 bits 數那要將 `data` 剩下的 bits 寫到下個 byte 用的手法也跟上面差不多,只是 mask 改成使用 `write_mask[bitsize - write_rhs]` 因為要寫入的 bit 是從前面開始寫入必須清成零,而後面維持原來的值
:::danger
* `if ((bitsize - write_lhs) > 0)` 這邊我覺得很奇怪,看下面所做的處理應該是要將 `write_rhs`後面幾個 bit 維持原來的值是用在只寫入 buffer 中間個 bit 時頭尾都應該保留原來的值,所以我覺得判斷式應該要改成 `write_rhs > bitsize` , `write_mask[8-(write_rhs - bitsize)]`
:::
> 程式碼是給你修改,不是拿來背誦的,路見不平,拿 patch 出來!
> :notes: jserv
>==有修改過,還有測試過別的case,原本的因為 input 都是1 output 都是0所以看不出來。==
```cpp
original = *dest;
if (write_lhs > 0) {
mask = read_mask[write_lhs];
if (bitsize > write_rhs) {
*dest++ = (original & mask) | (data >> write_lhs);
original = *dest & write_mask[bitsize - write_rhs];
*dest = original | (data << write_rhs);
} else {
if (bitsize < write_rhs)
mask = mask | write_mask[8 - (write_rhs - bitsize)];
*dest++ = (original & mask) | (data >> write_lhs);
}
} else {
if (bitsize < 8)
data = data | (original & write_mask[bitsize]);
*dest++ = data;
}
```
:::success
延伸問題:
1. 解釋上述程式運作的原理,改寫為更精簡且等效的程式碼;
2. 考慮到 alignment 和 word size,改寫為執行效率更高的程式碼;
3. 在 Linux 核心原始程式碼找出逐 bit 進行資料複製的程式碼,並解說相對應的情境;
> 提示: 觀察 [linux/bitops.h](https://github.com/torvalds/linux/blob/master/tools/include/linux/bitops.h) 裡頭巨集在原始程式碼的運用狀況
:::
---
## 測驗 `2`
### 程式運作原理
* `STRUCT_BODY(type)` 與 [quiz2](https://hackmd.io/@Hsuhaoxiang/quiz2) 對於儲存長字串的手法相似,只不過改成能用不同的資料型態,所以有一個 `type` 參數
```cpp
#define STRUCT_BODY(type) \
struct { \
size_t size : 54, on_heap : 1, capacity : 6, flag1 : 1, flag2 : 1,\
flag3 : 1; \
type *ptr; \
}
```
* `NEXT_POWER_OF_2(s)` 這邊的 s 是 vector 的 `size` 從名字可以看出是找大於等於 `s` 的2的冪次,目的是為了記憶體的對齊,例如 `s = 3` 的話就該回傳4,還利用了第二次作業學到的硬體指令 `__builtin_clzl` (count leading zero unsigned long) 找出第一個非零 bit 前面有幾個零,再讓1往左 shift `64-clz(s)` ,例如 3 = 0b11 前面有62個零需要向左 shift 2 個 bits 變成 0b100 回傳4,而 `(s & (s - 1)) == 0` 在 `s` 是二的冪次方時會成立,例如16 = 0b10000 ,15 = 0b01111 , 15&16 == 0,==VV1 = (s & (s - 1)) == 0==。
```cpp
#define NEXT_POWER_OF_2(s) \
(s & (s - 1)) == 0 \
? s \
: (size_t) 1 << (64 - __builtin_clzl(s)) /* next power of 2 */
```
* v(t, s, name,...) 這個 macro 裡面也用了 [quiz2](https://hackmd.io/@Hsuhaoxiang/quiz2) 中 union 的手法, 而 name 為宣告的 vector 名稱後面接的 `__attribute__((cleanup(vec_free)))` 就是當變數離開他的 scope 時會自動呼叫 `vec_free` 這個自己定義的函示, `__attribute__` 是 GCC extension 可以參考[你所不知道的C語言:技巧篇](https://hackmd.io/@sysprog/c-trick),`{.buf = {__VA_ARGS__}}`則是在初始化 vector 中的成員,其中 `{__VA_ARGS__}` 就是一開始 `name` 後面的 `...`,可以是複數個參數。
* `name.size` 這邊是計算 vector 中有多少元素, `__VA_ARGS__` 就是存入 vector 的元素,但為什麼要加上0然後最後再減掉1 ? 原本想說是宣告空的 vector 時才不會出問題,但實際改了之後發現還是能順利的算出現在 vector 中有的元素個數,還要再想想為什麼要這樣算。
* `sizeof((__typeof__(name.buf[0])[]){__VA_ARGS__}) / sizeof(name.buf[0]);`
* `name.capacity` 是實際分配出的記憶體大小,算出整個 array 大小再除以一個元素大小就能得到了,也可以初始化為 `NEXT_POWER_OF_2(s)`
:::warning
main()
{
v(int, 0, vec3);
printf("vec3 size:%zu\n",vec3.size);
return 0;
}
輸出:vec3 size:0
:::
```cpp
#define v(t, s, name,...) \
union { \
STRUCT_BODY(t); \
struct { \
size_t filler; \
t buf[NEXT_POWER_OF_2(s)]; \
}; \
} name __attribute__((cleanup(vec_free))) = {.buf = {__VA_ARGS__}}; \
name.size = sizeof((__typeof__(name.buf[0])[]){0, __VA_ARGS__}) / \
sizeof(name.buf[0]) - 1; \
name.capacity = sizeof(name.buf) / sizeof(name.buf[0])
```
* `__vec_reserve` 確保 vector 有 `n` 以上的 `capacity` 如果不夠則使用 `relloc` 或是 `malloc` 分配記憶體。
```cpp
static NON_NULL void __vec_reserve(void *vec,
size_t n,
size_t elemsize,
size_t capacity)
{
union {
STRUCT_BODY(void);
struct {
size_t filler;
char buf[];
};
} *v = vec;
if (n > capacity) {
if (v->on_heap) {
v->ptr = realloc(v->ptr,
elemsize * (size_t) 1 << (v->capacity = ilog2(n)));
} else {
void *tmp =
malloc(elemsize * (size_t) 1 << (v->capacity = ilog2(n)));
memcpy(tmp, v->buf, elemsize * v->size);
v->ptr = tmp;
v->on_heap = 1;
}
}
}
```
* `__vec_push_back` 中我看最久才看懂的地方是 `&v->ptr[v->size++ * elemsize]`為何要乘上 `elemsize` ?因為一開始就將 `v` 的 `STRUCT_BODY` 的資料型別用 `char` 只佔1 byte,所以再乘上 `elemsize` 才是正確的記憶體位置。
```cpp
static NON_NULL void __vec_push_back(void *restrict vec,
void *restrict e,
size_t elemsize,
size_t capacity)
{
union {
STRUCT_BODY(char);
struct {
size_t filler;
char buf[];
};
} *v = vec;
if (v->on_heap) {
if (v->size == capacity)
v->ptr = realloc(v->ptr, elemsize * (size_t) 1 << ++v->capacity);
memcpy(&v->ptr[v->size++ * elemsize], e, elemsize);
} else {
if (v->size == capacity) {
void *tmp =
malloc(elemsize * (size_t) 1 << (v->capacity = capacity + 1));
memcpy(tmp, v->buf, elemsize * v->size);
v->ptr = tmp;
v->on_heap = 1;
memcpy(&v->ptr[v->size++ * elemsize], e, elemsize);
} else
memcpy(&v->buf[v->size++ * elemsize], e, elemsize);
}
}
```
* `#define vec_pop_back(v) (void) (VV2)`
* 有兩個選項看 function 名稱可以知道將元素從 vector 移除,因為size 是目前元素個數只要將 size 減一 , push 時就會從目前 `size+1` 開始,看起來就是從vector最後一個元素加入元素一樣,事實上元素沒有移除掉,所以 ==VV2 = v.size -= 1==
* `vec_pop_back(v)` 沒有先確認目前 vector 的 size 所以當一個空的 vector pop 時會出問題。
```cpp
#define vec_pop_back(v) \
if(v.size>0) (v.size -= 1); \
else fprintf( stderr, "warning! vector is empty\n")
```
### 效率分析
:::success
延伸問題:
1. 解釋上述程式運作的原理,比照 [quiz2](https://hackmd.io/@sysprog/BJXNYx5NL) 進行效率分析;
2. 指出上述實作的限制並提出改進方案;
3. 參照 [folly::fbvector](https://github.com/facebook/folly/blob/master/folly/docs/FBVector.md) 文件和對應原始程式碼,以上述程式為基礎,實作 vector 經典操作
:::