# 2020q1 Homework3 (quiz3)
contributed by < `KYG-yaya573142` >
> [第 3 週測驗題](https://hackmd.io/@sysprog/linux2020-quiz3)
## 測驗 1
### XOR linked list 原理
XOR 運算特性為
* $A \oplus A = 0$
* $A \oplus 0 = A$
* $A \oplus 1 = \neg A$
* $(A \oplus B) \oplus B = A$
```cpp
typedef struct __list list;
struct __list {
int data;
struct __list *addr;
};
```
當初填做答表單會全錯就是因為我沒理解 [XOR linked list](https://en.wikipedia.org/wiki/XOR_linked_list) 實作的核心邏輯,在 XOR linked list 中 node 儲存的 `addr` 內容是 address of previous node (之後簡稱 prev) 及 address of next node (之後簡稱 next) 以 XOR 運算的結果 `addr = prev ^ next`,根據上述 XOR 的運算特性,可以用以下方式在 node 間移動
* `addr ^ prev = next`
* `addr ^ next = prev`
而在 list head 和 list tail 的 node,根據 $A \oplus 0 = A$ 可知其儲存的 `addr` 剛好都是鄰近的唯一 node 的指標
* `0 ^ next = addr = next`
* `prev ^ 0 = addr = prev`
```graphviz
digraph xs {
rankdir=LR;
node [shape=record]; //default node style
node1 [label="addr1|(0 ^ addr2)", xlabel="head"];
node2 [label="addr2|(addr1 ^ addr3)"];
node3 [label="addr3|(addr2 ^ addr4)"];
node4 [label="addr4|(addr3 ^ 0)", xlabel="tail"];
node1->node2->node3->node4
}
```
```cpp
#define XOR(a, b) ((list *) ((uintptr_t) (a) ^ (uintptr_t) (b)))
void insert_node(list **l, int d) {
list *tmp = malloc(sizeof(list));
tmp->data = d;
if (!(*l)) {
tmp->addr = NULL;
} else {
(*l)->addr = XOR(tmp, (*l)->addr);
tmp->addr = *l;
}
*l = tmp;
}
```
* 從 list head 新增 node,所以原本 list head 的 `*l` 需要更新儲存的 `addr` 為 next node 與 prev node 的 XOR
* `*l` 原本位於 list head,`(*l)->addr` 直接是 next node 的位置
* 新增的 `tmp` 就是 prev node
* 若尚無任何 node,新增的 node 會是唯一的 node 所以儲存的指標為 `NULL`
```cpp
void delete_list(list *l) {
while (l) {
list *next = l->addr;
if (next)
next->addr = XOR(next->addr, l);
free(l);
l = next;
}
}
```
* 從 list head 開始依序 `free` 掉 node
* `l` 原本位於 list head,`l->addr` 直接是 next node 的位置
* `free` 掉 `l` 以前須先更新 next node 中儲存的位置,因為原本的 next node 會變成新的 list head
* `XOR(next->addr, l)` 的結果會是 `next` 的下個 node 的位置
* `while` 直到 `free` 完所有 node
### 解題思路
```cpp=
list *sort(list *start)
{
if (!start || !start->addr)
return start;
list *left = start, *right = start->addr;
left->addr = NULL;
right->addr = XOR(right->addr, left);
left = sort(left);
right = sort(right);
...
```
* 6~11 行的部分利用遞迴的方式分割 list,但一次遞迴只分割出一個 node
* `left` 指向分割出的單一 node
* `right` 指向剩下的 node
* `start` 會指向已經排序完成的 list head
```cpp=12
...
for (list *merge = NULL; left || right;) {
if (!right || (left && left->data < right->data)) {
list *next = left->addr;
if (next)
next->addr = XOR(left, next->addr);
if (!merge) {
start = merge = left;
merge->addr = NULL;
} else {
merge->addr = LL1;
left->addr = LL2;
merge = left;
}
left = next;
} else {
...
```
* 第 13 行開始執行 merge sort
* `if (!right || (left && left->data < right->data))` 是實行排序判斷的位置,採用遞增順序所以數值小的排在前面
* 先探討將 `left` 的 node 接上 `merge` 的情況 (也就是 `left->data` 比較小,或是奇數 node 的情況)
* 15~17 行先將 node 從 list 分離出來
* 此時 `left` 指向這個分離出來的單一 node
* `next` 指向剩餘的 list
* 19~21 行代表此時 `merge` 無任何 node
* `left` 會是唯一的 node,所以儲存的指標為 `NULL`
* `start` 會指向已經排序完成的 list head,所以也是指向 `left`
* 22~25 行要將 `left` 接在 `merge` 的前面
* `merge` 是原本的 list head,`merge->addr` 直接是 next node 的位置
* `merge` 的 prev node 就是前面接上的 `left`,
* `merge->addr` 須更新為 `prev node ^ next node`,因此 `LL1` 應填入 `XOR(merge->addr, left)`
* `left` 會是新的 list head,所以 `left->addr` 會直接指向 next node,因此 `LL2` 應填入 `merge`
```cpp=27
...
} else {
list *next = right->addr;
if (next)
next->addr = XOR(right, next->addr);
if (!merge) {
start = merge = right;
merge->addr = NULL;
} else {
merge->addr = RR1;
right->addr = RR2;
merge = right;
}
right = next;
}
}
return start;
}
```
* 整體的邏輯與將 `left` 的 node 接上 `merge` 時的邏輯一樣,只是改為將 `right` 接上,就不再贅述原理
* `RR1` 應填入 `XOR(merge->addr, right)`
* `RR2` 應填入 `merge`
### 改善程式碼
#### `delete_list`
後來實際使用時,發現 `delete_list(queue)` 後不會將 `queue` 指標設為 NULL,這會導致接下來 `insert_node(&queue)` 會發生 segmentation fault
```cpp
int main(int argc, char const *argv[])
{
list *queue = NULL;
insert_node(&queue, 666);
delete_node(queue);
insert_node(&queue, 777); //seg. fault
}
```
因此我將 `delete_list` 改為使用 pointer to pointer 來實作
```cpp
void delete_list(list **l) {
while (*l) {
list *next = (*l)->addr;
if (next)
next->addr = XOR(next->addr, *l);
free(*l);
*l = next;
}
}
```
#### `show`
原本的實作缺乏簡單展示 list 內容的函式,而且 XOR linked-list 處理起來較複雜,因此實作一個輔助函式來顯示 list 的所有內容
```cpp
void show(list *target)
{
if (!target) {
printf("empty list\n");
return;
}
printf("head-> ");
list *prev = NULL, *next = target;
while(target) {
printf ("%d ", target->data);
next = XOR(target->addr, prev);
prev = target;
target = next;
}
printf("<-tail\n");
return;
}
```
### 最佳化策略 - 修改 `sort` 分割 node 的方式
原本 `sort` 中每次只會分割出一個 node,參照 [lab-0](https://hackmd.io/_a-DyVJ6T2SwYZO4kZLyNQ) 中 [merge sort](https://github.com/KYG-yaya573142/lab0-c/blob/73554501e945a60b23548de4d20b6941a166f3ac/queue.c#L195) 的做法,修改為每次會將 list 分為一半。實作的程式碼如下,明顯有使用過多變數的缺點,但現階段還想不到更漂亮的寫法
```cpp
list *merge_sort(list *start)
{
if (!start || !start->addr)
return start;
list *left = start, *right = start->addr;
list *tmp1 = start, *tmp2 = start->addr;
list *prev = NULL, *next = start;
/* split list into half */
while (right && XOR(right->addr, tmp1)) {
/* advance right by 2 nodes */
right = XOR(right->addr, tmp1);
tmp1 = right;
right = XOR(right->addr, tmp2);
tmp2 = right;
/* advance left by 1 node */
next = XOR(left->addr, prev);
prev = left;
left = next;
}
right = XOR(left->addr, prev);
right->addr = XOR(right->addr, left);
left->addr = XOR(left->addr, right);
left = merge_sort(start);
right = merge_sort(right);
...
```
以下列程式碼檢查,結果正確
```cpp
#define total_nodes 100
int main(int argc, char const *argv[])
{
list *queue = NULL;
srand(time(NULL));
for (int i = 0; i < total_nodes; i++) {
insert_node(&queue, (rand() % 100));
}
queue = merge_sort(queue);
show(queue);
}
```
#### 效能差異
使用以下程式碼進行測試
```cpp
#define total_nodes 1000
#define sample_size 100
int main(int argc, char const *argv[])
{
struct timespec t_start, t_end;
list *queue1 = NULL;
srand(time(NULL));
FILE *fp = fopen("./out_statistic", "w");
for (int i = 1; i < total_nodes + 1; i++) {
double t1[sample_size] = {0};
double mean1 = 0.0, sd1 = 0.0, result1 = 0.0;
int count1 = 0;
/* measure sample_size times of data and
* remove outliers based on the 95% confidence level
*/
for (int n = 0; n < sample_size; n++) {
/* generate linked-list for testing */
for (int node = 0; node < i; node++)
insert_node(&queue1, (rand() % 100));
clock_gettime(CLOCK_MONOTONIC, &t_start);
queue1 = merge_sort(queue1, i); /* measurement */
clock_gettime(CLOCK_MONOTONIC, &t_end);
t1[n] = (long long)(t_end.tv_sec * 1e9 + t_end.tv_nsec)
- (long long)(t_start.tv_sec * 1e9 + t_start.tv_nsec);
mean1 += t1[n]; //sum
delete_list(&queue1);
}
mean1 /= sample_size; //mean
for (int n = 0; n < sample_size; n++) {
sd1 += (t1[n] - mean1) * (t1[n] - mean1);
}
sd1 = sqrt(sd1 / (sample_size - 1)); //standard deviation
for (int n = 0; n < sample_size; n++) { //remove outliers
if (t1[n] <= (mean1 + 2 * sd1) && t1[n] >= (mean1 - 2 * sd1)) {
result1 += t1[n];
count1++;
}
}
result1 /= count1;
fprintf(fp, "%d %.5lf samples: %d\n", i, (result1/1000), count1);
assert(count1 > 80); /* remove too many samples, considered unrepresentative */
}
fclose(fp);
return 0;
}
```
![](https://i.imgur.com/Id6SjDB.png)
* 排序所需時間明顯降低
### 最佳化策略 - 使用 tiled merge sort
Wiki 的 Merge sort 中談到 [Optimizing merge sort](https://en.wikipedia.org/wiki/Merge_sort#Optimizing_merge_sort) 的策略
> For example, the tiled merge sort algorithm stops partitioning subarrays when subarrays of size S are reached, where S is the number of data items fitting into a CPU's cache. Each of these subarrays is sorted with an in-place sorting algorithm such as insertion sort, to discourage memory swaps, and normal merge sort is then completed in the standard recursive fashion.
* 先將子序列分割,直到子序列小於某個閾值 S 就停止分割
* S 值為可完整放進處理器 cache 中的元素數量
* 每個大小為 S 的子序列,都透過適用於小規模 in-place (即不需要額外配置記憶體) 排序演算法進行排序,減少在記憶體內交換特定區域的次數
#### CPU 硬體組態
* Intel(R) Core(TM) i7-3770 CPU @ 3.40GHz
* 4 CPU cores
* 8 threads
* L1i cache: 32 KB (x4 cores)
* L1d cache: 32 KB (x4 cores)
* 8-way set associative
* L2 cache: 256 KB (x4 cores)
* 8-way set associative
* L3 cache: 8 MB
* 16-way set associative shared cache
剛好可以用 CSAPP Cache memories 講義中的範例
![](https://i.imgur.com/S1DofCh.png)
* 64 bytes per block (cache line)
* 8 blocks per set
* total 64 sets
* node 的大小為 16 bytes
* 一個 block 可以存放 4 nodes
* $4 (nodes)\times 64(sets)=256$ 個連續的 node 會用掉所有 set 中的一個 block (因為是 set associative)
* $256\times 8=2048$ 個 node 會剛好用完 L1 cache
* 但這只是理想的算法,快取還需要存放除了 node 以外的資料,且每個 node 的記憶體位置不一定是連續的
#### in-place algorithm - insertion sort
我選用 insertion sort 做為 in-place 排序演算法
```cpp
list *insert_sort(list *head)
{
if (!head || !head->addr)
return head;
/* pick first node from head */
list *result = head;
head = head->addr;
head->addr = XOR(result, head->addr);
result->addr = NULL;
while (head) {
/* detach node from head */
list *next = head->addr;
if (next)
next->addr = XOR(head, next->addr);
/* insertion sort */
list *node = result, *prev = NULL, *tmp = NULL;
while (node && node->data < head->data) {
tmp = node;
node = XOR(prev, node->addr);
prev = tmp;
}
if (!prev) { /* insert at head */
head->addr = result;
result->addr = XOR(result->addr, head);
result = head;
}
if (!node) { /* insert at tail */
head->addr = prev;
prev->addr = XOR(prev->addr, head);
}
if (prev && node) { /* insert in between prev and node */
prev->addr = XOR(XOR(prev->addr, node), head);
node->addr = XOR(head, XOR(node->addr, prev));
head->addr = XOR(prev, node);
}
head = next;
}
return result;
}
```
#### 找出適當的 S
修改原本 merge sort 部分的程式碼
```cpp
list *merge_sort(list *start, int count)
{
...
/* split list */
...
/* use in-place alg. when list size <= threshold */
if ((count >> 1) > threshold) {
left = merge_sort(start, (count >> 1) + (count & 0x1));
right = merge_sort(right, count >> 1);
} else {
left = insert_sort(start);
right = insert_sort(right);
}
...
```
* 當子序列數量低於 S 就切換成使用 insertion sort
* `threshold` 就是閾值 S,直接設為 global variable 以方便實驗時進行更動
* 為了方便計算子序列資料數量,直接把總資料數量 `count` 當作參數帶進 `merge_sort`
* 最後一樣用 merge sort 來合併所有子序列
實驗的程式碼如下
```cpp
#define total_nodes 1000
int main(int argc, char const *argv[])
{
list *queue = NULL;
struct timespec t_start, t_end;
srand(time(NULL));
for (threshold = 0; threshold < 500; threshold++) { /* use different threshold */
for (int i = 0; i < total_nodes; i++) { /* generate random list */
insert_node(&queue, (rand() % 100));
}
clock_gettime(CLOCK_MONOTONIC, &t_start);
queue = merge_sort(queue, total_nodes);
clock_gettime(CLOCK_MONOTONIC, &t_end);
long long t1 = (long long)(t_end.tv_sec * 1e9 + t_end.tv_nsec)
- (long long)(t_start.tv_sec * 1e9 + t_start.tv_nsec);
printf("%d %lld\n", threshold, (t1/1000));
delete_list(&queue);
}
return 0;
}
```
* 量測不同 S 時,排序所需的時間
* 實驗的 list 總共有 1000 nodes
![](https://i.imgur.com/m8UxwHd.png)
* 水平黑線是原本 [`merge_sort`](#最佳化策略---修改-sort-分割-node-的方式) 排序 1000 個 nodes 的所需時間
* 分別在 62、125、250、500 有明顯的速度落差
* 當 S 在 125 以內時,排序所需的時間會快於單純使用 merge sort
* S 在 61 以內時具有最快的速度
:::warning
原本以為這漂亮的圖有特別的意義,後來才想到一件事情
* 每次 merge sort 都會把 list 分為一半
* 所以我使用的 1000 nodes 經過分割後,產生的子序列只會有幾種固定的大小,剛好是 500、250、125、62 ...
* 例如當 S 在 126~250 之間時,實際能被 insertion sort 處理的子序列大小永遠是 125,因為上一個子序列大小是 250
:::
![](https://i.imgur.com/6pqTL5o.png)
* 比對原本的 merge sort 可以看到所需時間降低
* 會有階梯狀的原因如上述說明,是因為子序列大小每次都是對半分造成的
#### 使用 CSAPP 的 cache lab
本來在思考是否有方法可以了解 merge sort 在不同 S 下使用 cache 的情況,發現可以使用 [Valgrind - Cachegrind](https://valgrind.org/docs/manual/cg-manual.html),但同時也想到之前在 CSAPP 的 [cache lab](http://csapp.cs.cmu.edu/3e/labs.html) 中有提供一個模擬 cache 行為的程式,因此決定先嘗試這個比較簡單的方法
* 首先在程式碼本身盡量移除跟 merge sort 無關的部分,避免干擾
```cpp
#define total_nodes 1000
int main(int argc, char const *argv[])
{
list *queue = NULL;
srand(time(NULL));
for (int i = 0; i < total_nodes; i++) { /* generate random list */
insert_node(&queue, (rand() % 100));
}
queue = merge_sort(&queue, total_nodes);
return 0;
}
```
* 使用 valgrind 來紀錄不同 S 值下執行程式的 address trace
```shell
$ valgrind --log-fd=1 --tool=lackey -v --trace-mem=yes ./test > out
```
* 接著使用 CSAPP 的作業參考範例 `csim-ref` 來模擬,使用的參數是對照 [CPU-硬體組態](#CPU-硬體組態) 設置的
```shell
$ ./csim-ref -s 6 -E 8 -b 6 -t out
```
#### S = 0 (單純 merge sort)
```
hits:609821 misses:3846 evictions:3334
```
#### S = 50
```
hits:519978 misses:3764 evictions:3252
```
#### S = 100
```
hits:591181 misses:3759 evictions:3247
```
#### S = 200
```
hits:737422 misses:3733 evictions:3221
```
#### S = 400
```
hits:1140759 misses:3730 evictions:3218
```
* cache eviction 和 cache miss 都沒有明顯的差異,但是 cache hit 隨著 S 提高而增加
* 在假設模擬器正確的前提下,可以推斷出
* 在測試的範圍內,都不會出現 L1 cache 不夠用的情況
* 提高 S 確實能讓處理器更善用 L1 cache,但由於 insertion sort 比 merge sort 慢,過高的 S 會導致 insertion sort 的效能劣勢將 cache 的優勢抵銷,甚至是降低整體的效能
### 修改 lab0-c 裡頭的 harness.c (待補)
## 測驗 2
### 解題思路
```cpp
#include <stddef.h>
struct node {
int data;
struct node *next;
} *head;
void insert(struct node *newt) {
struct node *node = head, *prev = NULL;
while (node != NULL && node->data < newt->data) {
prev = node;
node = node->next;
}
newt->next = node;
if (prev == NULL)
head = newt;
else
prev->next = newt;
}
```
* `while` 迴圈實際上是在排序,將 `newt` 根據資料大小插入 list 中適當的位置
```cpp
void insert(struct node *newt)
{
struct node **link = &head;
while (*link && AA)
BB;
newt->next = *link;
*link = newt;
}
```
* `link` 是 pointer to pointer,可以理解為一般 pointer 儲存的是某個變數的位置,而 pointer to pointer 儲存的就是某個指標的位置,因此 `*link` 是指標
* `while` 迴圈做的事情一樣是排序,必須先對 `link` 進行 dereference 一次才會得到指向 list element 的指標,因此 `AA` 應填入 `(*link)->data < newt->data`
* pointer to pointer 儲存的是指標的位置,因此必須使用取址運算子 `&` 來取用指標的位置,因此 `BB` 應填入 `link = &(*link)->next`
### 對執行時間與 [Branch predictor](https://en.wikipedia.org/wiki/Branch_predictor) 的影響
使用以下程式碼進行實驗,分別測試兩種不同的 `insert` 的所需時間
```cpp
int main(int argc, char const *argv[])
{
struct timespec t_start, t_end;
head = NULL;
for (int i = 0; i < 1000; i++) {
struct node *newt = malloc(sizeof(struct node));
newt->data = i;
newt->next = NULL;
clock_gettime(CLOCK_MONOTONIC, &t_start);
//insert(newt);
insert_ptp(newt);
clock_gettime(CLOCK_MONOTONIC, &t_end);
long long t1 = (long long)(t_end.tv_sec * 1e9 + t_end.tv_nsec)
- (long long)(t_start.tv_sec * 1e9 + t_start.tv_nsec);
printf("%d %lld\n", i, t1);
}
return 0;
}
```
![](https://i.imgur.com/g7lAgmY.png)
* 無顯著的差異,可能要進一步提升測試的資料總數
使用 `perf` 來分析 branch 的差異
```shell
$ sudo perf stat --repeat 5 -e branch-instructions:u,branch-misses:u ./test
```
* `insert`
```
Performance counter stats for './test' (5 runs):
1,383,346 branch-instructions:u ( +- 0.01% )
10,871 branch-misses:u # 0.79% of all branches ( +- 1.04% )
0.004962 +- 0.000679 seconds time elapsed ( +- 13.68% )
```
* `insert_ptp`
```
Performance counter stats for './test' (5 runs):
1,382,351 branch-instructions:u ( +- 0.00% )
10,659 branch-misses:u # 0.77% of all branches ( +- 0.75% )
0.004709 +- 0.000558 seconds time elapsed ( +- 11.85% )
```
* `insert_ptp` 在 branch-instructions:u 還是 branch-misses:u 有些微的改善,但是不明顯
嘗試改用 [perf record & perf report](http://wiki.csie.ncku.edu.tw/embedded/perf-tutorial#perf-record-perf-report) 來分析
```shell
$ sudo perf record -F 100000 -e branch-misses:u,branch-instructions:u ./test
$ sudo perf report
```
* `insert`
```
333 branch-misses:u ◆
919 branch-instructions:u
```
```
Samples: 919 of event 'branch-instructions:u', Event count (approx.): 1386496
Overhead Command Shared Object Symbol
69.75% test test [.] insert
6.16% test libc-2.27.so [.] vfprintf
5.70% test libc-2.27.so [.] _IO_file_xsputn@@GLIBC_2.2.5
2.57% test libc-2.27.so [.] _int_malloc
```
```
Samples: 333 of event 'branch-misses:u', Event count (approx.): 11279
Overhead Command Shared Object Symbol
16.23% test libc-2.27.so [.] _dl_addr
12.54% test test [.] insert
11.38% test libc-2.27.so [.] printf
9.02% test libc-2.27.so [.] vfprintf
8.80% test libc-2.27.so [.] _IO_do_write@@GLIBC_2.2.5
7.87% test test [.] main
```
* `insert_ptp`
```
365 branch-misses:u ◆
947 branch-instructions:u
```
```
Samples: 947 of event 'branch-instructions:u', Event count (approx.): 1385333
Overhead Command Shared Object Symbol
72.25% test test [.] insert_ptp
7.83% test libc-2.27.so [.] vfprintf
3.23% test libc-2.27.so [.] _int_malloc
```
```
Samples: 365 of event 'branch-misses:u', Event count (approx.): 11535
Overhead Command Shared Object Symbol
17.59% test libc-2.27.so [.] _dl_addr
15.87% test test [.] main
14.58% test libc-2.27.so [.] _IO_file_xsputn@@GLIBC_2.2.5
11.35% test libc-2.27.so [.] _IO_file_write@@GLIBC_2.2.5
9.71% test libc-2.27.so [.] vfprintf
7.63% test libc-2.27.so [.] _IO_do_write@@GLIBC_2.2.5
5.21% test test [.] insert_ptp
```
* 從 branch-misses:u 的詳細報告就可以看出差異
* `insert` 造成的 branch-misses 占了 333 筆資料的 12.54%
* `insert_ptp` 造成的 branch-misses 占了 365 筆資料的 5.21%
###### tags: `linux 2020`