---
tags: 進階電腦系統理論與實作
---
# 2020q3 Homework11 (quiz11)
contributed by < `RinHizakura` >
> [第 11 週測驗題](https://hackmd.io/@sysprog/2020-quiz11)
> [GitHub](https://github.com/RinHizakura/vector)
## 測驗 `1`
:::info
內容搬運自之前在測驗題 [quiz4](https://hackmd.io/@RinHizakura/B17sUnk4v) 時所記錄
:::
### 實作原理
#### `v`
```c=
#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])
```
* 在 Union 中有兩種 struct。一種是透過 `STRUCT_BODY` 定義的。一種是有一個 `size_t` 的成員 `filter`,和 `NEXT_POWER_OF_2(s)` 大小的 `t` 型態 array。
* 根據 [cleanup (cleanup_function)](https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-Attributes.html) `__attribute__((cleanup(vec_free)))` 會在變數離開 stack 時自動呼叫 `vec_free`。
* 根據 [Variadic Macros](https://gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html),把 `name` 以後的參數對應到 `__VA_ARGS__`,用來初始化 `buf` 裡的內容。
* `size` 計算 vector 目前儲存的數量,透過 `(__typeof__(name.buf[0]))` 得到 buf 的型別,再用此型別去建立一個成員有 `{0, __VA_ARGS__}` 的 literal,則 vector 大小即是這個 literal 需要的儲存空間除以單個元素的儲存空間再減 1(扣掉 0)
* 需要多放一個 0,是避免沒有 `__VA_ARGS__` 時 buf[0] 不會出錯
* `capacity` 是 vector 可以容納的數量
#### `STRUCT_BODY`
這個 macro 用來宣告 struct 結構,其中 `type` 是結構中 pointer 的型態。前面的 size_t 則是透過在 [C 語言:bit-field](https://hackmd.io/@RinHizakura/Hk1CYv1kv/%2F5B7s4KiaQOmS0Ud6Px6Cww) 提到的技巧,把 `size_t` 的 64 位元分配給不同的 struct member。
```c=
#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)`
```c=
#define NEXT_POWER_OF_2(s) \
VV1 \
? s \
: (size_t) 1 << (64 - __builtin_clzl(s)) /* next power of 2 */
```
求出比 `s` 大且最接近 `s` 的 2^n 次方(n 為整數)。因此 `VV1 = (d)(s & (s - 1)) == 0` (可以參閱 [C 語言:數值系統篇](https://hackmd.io/9tGfpJtLTwyyOzDvlyJS2w?view#x-amp-x---1--0)),如果 s 本身就是 2^n 次方則直接是 s,否則透過 `64 - __builtin_clzl` 求出 s 從左邊數來第 1 個 1 在哪,位移那個量可以得到所求。
#### `NON_NULL`
```c=
#define NON_NULL __attribute__((nonnull))
```
[__attribute__((nonnull))](https://www.keil.com/support/man/docs/armcc/armcc_chr1359124976631.htm) 告訴編譯器這個 function 不能接受 NULL pointer,否則會在編譯時期產生警告。
#### `vec_free`
```c=
static NON_NULL void vec_free(void *p)
{
STRUCT_BODY(void) *s = p;
if (s->on_heap)
free(s->ptr);
}
```
檢查 vector 是否是用 heap 來儲存,是的話要透過 free 來釋放。
#### `vec_size`
```c=
#define vec_size(v) v.size
```
如你所見,不解釋。
#### `vec_capacity`
```c=
#define vec_capacity(v) \
(v.on_heap ? (size_t) 1 << v.capacity : sizeof(v.buf) / sizeof(v.buf[0]))
```
檢查 vector 是否額外使用 heap 儲存資料,如果是,$2^{v.capacity}$ 為實際的容量大小,否則就是 `sizeof(v.buf) / sizeof(v.buf[0]))`。
#### `vec_data`
```c=
#define vec_data(v) (v.on_heap ? v.ptr : v.buf) /* always contiguous buffer */
```
針對是否使用 heap 存取的 member 不同。
#### `vec_elemsize`
```c=
#define vec_elemsize(v) sizeof(v.buf[0])
```
如你所見。
#### `vec_pos`
```c=
#define vec_pos(v, n) vec_data(v)[n] /* lvalue */
```
用來取 vector 的第 n 個值
#### `vec_reserve`
```c=
#define vec_reserve(v, n) __vec_reserve(&v, n, vec_elemsize(v), vec_capacity(v))
```
#### `__vec_reserve`
```c=
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;
}
}
}
```
透過 heap 擴大 vector 的 capacity
#### `ilog2`
```c=
static inline int ilog2(size_t n)
{
return 64 - __builtin_clzl(n) - ((n & (n - 1)) == 0);
}
```
求出 k,k 滿足比 n 大且最接近的 $2^k$(k 為整數)
#### `vec_push_back`
```c=
#define vec_push_back(v, e) \
__vec_push_back(&v, &(__typeof__(v.buf[0])[]){e}, vec_elemsize(v), \
vec_capacity(v))
```
#### `__vec_push_back`
```c=
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);
}
}
```
push 進一個元素,必要的時候要增加 capacity。
#### `vec_pop_back`
```c=
#define vec_pop_back(v) (void) (VV2)
```
拿出一個元素,所以 `VV2 = ( a )v.size -= 1`
### 實作限制與改進方案
#### `vec_pop_back`
```c=
#define vec_pop_back(v) assert(v.size > 0); v.size -= 1
```
原本的 `vec_pop_back` 僅僅是把 `size -= 1`,對於空的 vector 的 pop 應該要反映出問題,因此修改為此。
#### `vec_pos`
透過 assert 檢查超出邊界的存取,詳細請參閱前面的 Github 連結。
#### `v`
透過 [Static Assertion](https://en.wikichip.org/wiki/c/static_assertion) 避免初始化 void 型別的 vector,需注意到 static assertion 的檢查只能接受 [Constant Expressions](https://www.cs.auckland.ac.nz/references/unix/digital/AQTLTBTE/DOCU_066.HTM) 而不能執行 function,不過我們可以透過 GCC builtin 來完成字串比對。
```c=
_Static_assert(__builtin_strncmp(#t, "void", 4), "error type");
```
詳情請參閱 Github。
#### 記憶體配置
在 [folly::fbvector](https://github.com/facebook/folly/blob/master/folly/docs/FBVector.md) 提到記憶體配置原則的重要性。當 vector 需要更多空間時,一次 malloc(realloc) 要重新要多少空間是很關鍵的議題,如果要的太少,會導致頻繁呼叫 malloc 產生的時間成本,但是如果要的太多又會有空間浪費的問題。
空間的成長幅度稱為 growth factor,在原始程式中,growth factor 為 2,也就是當空間不足時,就把 heap 空間擴大成兩倍。但是在文章中指出此設計不好的地方。假如初始一塊大小為 C 的空間,則 growth factor = k,下一次需要的空間是原本的 K 倍:
$C$, $C * k$, $C * k^2$, $C * k^3$, ...
在 k = 2 的狀況下,因為 $C$ + $C * k$ + $C * k^2$ + ... $C * k^{n-1}$ < $C * k^n$ 恆成立,因此每次重新配置的空間都比之前配置過的總和大,導致每一次配置就真的需要多要記憶體,而沒辦法重新利用之前配置的空間(如果新的所需空間總和小於之前,則有機會從 pool 裡拿出之前配置的空間來使用就足夠)。
需注意到 allocator 實際上是怎麼分配會影響到實作的策略,不過 k = 2 不論如何都是理論上最差的選擇。而 fbvector 與 jemalloc 搭配,使用 growth factor 1.5 得到最佳效能。
- [ ] 研究如何設計使用者定義的 allocator,搭配記憶體的配置策略得到更佳的效能
### 實現經典 vector 操作
#### `front` / `back`
```c=
#define vec_front(v) vec_pos(v, 0)
#define vec_back(v) vec_pos(v, v.size-1)
```
透過 `vec_pos` 去存取也可以確保錯誤使用時(例如空 vector 時用 `vec_front`)被 assert 擋下來。
#### `insert`
C++ 中的 insert 根據參數功能會有點差異,其中最基本的功能是可以把指定的 element `e` 插入到 `p` 位置。
```c=
#define vec_insert(v, p, e) \
do { \
assert(p >= 0 && p < v.size); \
__typeof__(v.buf[0]) __tmp; \
for (int i = 0; i < (signed) v.size; i++) { \
if (i == p) { \
__tmp = vec_pos(v, i); \
vec_pos(v, i) = e; \
} else if (i > p) { \
__typeof__(v.buf[0]) __t; \
__t = __tmp; \
__tmp = vec_pos(v, i); \
vec_pos(v, i) = __t; \
} \
} \
vec_push_back(v, __tmp); \
} while (0)
```
```c=
#define vec_insert_n(v, p, n, e) \
do { \
assert(p >= 0 && p < v.size); \
assert(n > 0); \
__typeof__(v.buf[0]) *__tmp = \
malloc(sizeof(__typeof__(v.buf[0])) * (v.size - p)); \
int cnt = 0; \
for (int i = 0; i < (signed) v.size; i++) { \
if (i >= p) { \
__tmp[cnt++] = vec_pos(v, i); \
} \
} \
for (int i = 0; i < (signed) v.size - p; i++) \
vec_pop_back(v); \
vec_reserve(v, v.size + n + cnt); \
for (int i = 0; i < n; i++) \
vec_push_back(v, e); \
for (int i = 0; i < cnt; i++) \
vec_push_back(v, __tmp[i]); \
free(__tmp); \
} while (0)
```
作法稍微有點暴力,尤其是 `vec_insert_n`,可以實作出相同效果的方法有許多種,需要考量到效率去做改進。
:::warning
雖然用 macro 可以減少轉型,讓對 vector 的操作比較簡單,不過也因此如果認真要做 assert 可以有很多需要檢查的(在 int 的 vector 插入 float element? `vec_insert_n` 如果 insert 的數量是 float?)。如果要增加實用性的話,需要對這些層面去改善(真的 assert 去檢查?或者換成 function 型式讓 compiler 去檢查)。
:::