# 2023q1 Homework4 (quiz4)
contributed by < [Chiwawachiwawa](https://github.com/Chiwawachiwawa) >
## 測驗一
### 解釋並且補全上述程式碼
#### 插入 insert
```c
for (pathp = path; pathp->node; pathp++) { \
int cmp = pathp->cmp = x_cmp(node, pathp->node); \
assert(cmp != 0); \
if (cmp < 0) { \
pathp[1].node = rbtn_left_get(x_type, x_field, pathp->node); \
} else { \
pathp[1].node = rbtn_right_get(x_type, x_field, pathp->node); \
} \
} \
pathp->node = node;
```
此段程式碼用於尋找新節點應該插入的位址。它使用 cmp 來進行比較,並藉由 for 迴圈來選擇是往左邊還是往右邊尋找,直到找到最終的插入點。在這個過程中,有三種情況需要考慮:
1. 若新節點的值比目前節點的值小,則插入到目前節點的左子樹中。即 `pathp[1].node(new node)->node` 的左子樹。
2. 若新節點的值比目前節點的值大,則插入到目前節點的右子樹中。即 `pathp[1].node(new node)->node` 的右子樹。
3. 當 `pathp->node` 為 `NULL` 時,表示已找到最終的插入點,此時將新節點插入到這個位置即可。即 `pathp->node` 存放目前查找到的節點。
```c
for (pathp--; (uintptr_t) pathp >= (uintptr_t) path; pathp--) {
...
```
這段程式碼可分為幾個步驟。在確定好插入位置後,我們需要處理幾個情況。因為插入新節點後,需要對已插入的樹進行調整,以符合紅黑樹的定義。觀察程式碼,這次實作範例使用的是左傾紅黑樹 (LLRBT)。
:::warning
判斷的依據是什麼?
:notes: jserv
:::
:::info
:::
最後,我們將處理好的樹的節點顏色設定為黑色。
```c
rbtree->root = path->node;
rbtn_black_set(x_type, x_field, rbtree->root);
```
#### 刪除 remove
### 閱讀以下文章
Jemalloc:
jemalloc 最大的優勢還是其強大的 scalability,
對於 lock contention 是其最大的解決議題,其中我們來嘗試探討紅黑樹相關內容,
由於紅黑樹對平衡性的要求沒有 AVL 樹高,因此頻繁插入和刪除節點時,觸發平衡調整
的次數更少,平衡調整的過程也更易收斂。
我們來觀察插入以及刪除的相關程式碼
結構:
### 當插入時會有幾個情形進行討論
1.插入的紅色節點在左邊時:
case 1 : parent = black
直接完成
case 2 : parent = red
這樣會產生連續兩個紅色節點相連,這時我們需要將 `node->left->left` 的節點轉為黑色, `node->left` 轉為新的 node,並且還是維持紅色,因為我們要確保 `node->left` 的 parent_node 為黑色,調整之後:
```c
left_node = new_node(red)
left->left->node = left_node(black)
root(black) = right_node(black)
```
接下來,程式進行<s>回朔</s> [回溯](https://dict.revised.moe.edu.tw/dictView.jsp?ID=85125),因為新的 root 被改為 red。
當插入的節點(red)在右邊時:
case 1 : `(parent = NULL || black) && (parent == black)`
我們這個情況要進行左旋,調整後:
```c
new_node = new_parent
old_parent_color = black
old_parent = left_child && color = red
```
case 2 : `parent->left_node = NULL && (color == black)` 以這個條件下為前提,當 `parent_node color == red`
同樣進行左旋
調整後:
```c
new_node = new_parent
old_parent = left_child, color = red
do next loop...
```
下一個情況我們就會遇到前面提過的 case (`root->left && root->left->left = red`)(第一段的 case 2)。
case 3 : p`arent_node->left && color == red`
我們需要將 left&right_color set black,並且將 `parent_node` 的 color 改為 red 。
因為 parent_node 被改了顏色,所以我們需要一直進行回溯,直到我們遇到黑色的節點。
### 當刪除時會有幾個情形進行討論
如果刪除的節點為紅色,因為不會影響到平衡,故直接刪除即可,反之,則我們需要一直進行回溯,並且恢復平衡(路徑上的黑色節點要一樣),重複此動作,直至數量平衡。
接下來我們來看看刪除的相關程式:
```c
for (pathp = path; pathp->node != NULL; pathp++) { int cmp = pathp->cmp = a_cmp(node, pathp->node);...
```
if (cmp < 0)
這個情況我們做這一步就可以了
```c
pathp[1].node = rbtn_left_get(a_type, a_field, \
pathp->node)
```
但是
if (cmp == 0)
這時我們需要將後續的節點保存下來以便做處理
```c
pathp->cmp = 1;
nodep = pathp;
for (pathp++; pathp->node != NULL;
pathp++) {
pathp->cmp = -1;
pathp[1].node = rbtn_left_get(a_type, a_field,pathp->node);
}
break;
}
}
```
`if (pathp->node != node)`
用最後一個靠近的節點的 child 來代替此節點,並保存替代節點的顏色
```c
bool tred = rbtn_red_get(a_type, a_field, pathp->node);
```
然後把替代節點重新著色
```c
rbtn_color_set(a_type, a_field, pathp->node,
rbtn_red_get(a_type, a_field, node));
```
接下來跳過一些單純的部分,我想著重講一段我個人認為不好閱讀懂的地方
```c
if (rbtn_red_get(a_type, a_field, pathp->node)) {
assert(pathp[-1].cmp < 0);
rbtn_left_set(a_type, a_field, pathp[-1].node, NULL);
return;
}
```
這裡 `pathp->node` 就是需要刪除的節點,但是它的顏色經由上述忽略的程式碼可以看出,已經被替換他的節點顏色所取代,而且他在這顆紅黑數裡面的位置也被移除了,在 path 堆疊的位置也被置換到了頂端,
而這邊 assert 是指沒有右節點為紅色的紅黑樹,只有左節點為紅色節點的紅黑樹,那這時如果,右節點為紅色,插入時左節點為紅色,則他們同時需要變為黑色,如果左節點為空或是為黑色,則我們需要進行左旋,因此,這段程式碼,是用來確保紅色的節點只存在左邊的子節點,所以它 `parent` 的 `cmp < 0`。
在講解完一些比較難的地方之後,我要接這來對比 Linux 核心之中的一些優劣
這邊用到老師提供的專案 [rb-bench](https://github.com/jserv/rb-bench)。
### [rb-bench](https://github.com/jserv/rb-bench)
進行 `make image` 會得到一張效能統計圖片
![](https://i.imgur.com/bsONiJ8.png)
可以看到,在小單位測試樣本的情況下,我們會發現 "Linux" 的效能竟然不是最好的,不論線性還是隨機情況下輸入。
### 探討原因(觀察 FreeBSD 的 tree)
在SmallSetLinear情況下,我發現 FreeBSD 的效能最好,因此繼續閱讀相關內容。
### (FreeBSD,Linux,Jemalloc)共同使用到的功能:
在 test.h 之中,可以看出如何使用 random 機制來生成隨機數的。
首先觀察各個樹插入的相關議題。
### 研讀 FreeBSD 實作
FreeBSD `rb_tree` structures
```c
struct name { \
struct type *sph_root; /* root of the tree */ \
}
#define SPLAY_INITIALIZER(root) \
{ NULL }
#define SPLAY_INIT(root) do { \
(root)->sph_root = NULL; \
} while (/*CONSTCOND*/ 0)
#define SPLAY_ENTRY(type) \
struct { \
struct type *spe_left; /* left element */ \
struct type *spe_right; /* right element */ \
}
```
閱讀插入相關函式
#### `RB_INSERT` FreeBSD vs. Linux
```c
name##_RB_INSERT(struct name *head, struct type *elm) \
{ \
struct type *tmp; \
struct type **tmpp = &RB_ROOT(head); \
struct type *parent = NULL; \
\
while ((tmp = *tmpp) != NULL) { \
parent = tmp; \
__typeof(cmp(NULL, NULL)) comp = (cmp)(elm, parent); \
if (comp < 0) \
tmpp = &RB_LEFT(parent, field); \
else if (comp > 0) \
tmpp = &RB_RIGHT(parent, field); \
else \
return (parent); \
} \
return (name##_RB_INSERT_FINISH(head, parent, tmpp, elm)); \
}
```
我們先判斷 root 是否為空,如果不是的話,就進入 while 進行實作,
這邊我有一個問題,為何在 linux 之中,第一堂課有提到,
通常在判斷條件的情況下,會將 `(tmp = *tmpp) = NULL`放在前端,
那為何在這個程式裡面遇到這種情況(?
因此我想測試一下以下程式
```c
name##_RB_INSERT(struct name *head, struct type *elm)
{
struct type *tmp;
struct type **tmpp = RB_ROOT(head);
struct type *parent = NULL;
if (!(tmp = *tmpp)){
return(name##_RB_INSERT_FINISH(head, parent, tmpp, elm))
}
parent = tmp;
__typeof(cmp(NULL, NULL)) comp = (cmp)(elm, parent); \
if (comp < 0) \
tmpp = &RB_LEFT(parent, field); \插入左子
else if (comp > 0) \
tmpp = &RB_RIGHT(parent, field); \插入右子
else \
return (parent); \已經存在了
}
```
接下來我們探討一下為何 FreeBSD 的效率會高於 Linux 核心因此我要對照 Linux 核心近似於 `RB_LEFT` 的功能
#### `RB_LEFT`
```c
RB_LEFT(elm, field) _RB_LINK(elm, _RB_L, field)
```
#### `RB_LINK`
```c
_RB_LINK(elm, dir, field) (elm)->field.rbe_link[dir]
```
在 Linux 核心的插入過程中,
我發現,在 linux 之中關於插入的函式,比 `RB_INSERT` 多的實在是太多了,
但是 linux 的插入在我看來是直觀非常多的,
但是由於 FreeBSD 效能比較好,因此我決定按照他的風格來改寫一些 linux 上面的實作,
我觀察到呼叫流程,我發現 FreeBSD 對於紅黑樹的平衡有一些與 linux 的實作有一些不同的地方,他是額外獨立出一個函式來進行處理,在 insert 之中來透過 _RB_INSERT_FINISH 來進行呼叫
`if ((_RB_BITS(gpar) & _RB_LR) == 0)`
在我的初步閱讀下,我發現 BSD 紅黑樹十分注重「高度」,基本上對於樹結構的調整也是圍繞著這個議題打轉
我們可以看到:
`/* the rank of the tree rooted at elm grew */`
因為在 `_RB_INSERT_COLOR` 的開頭有提到 The balance criterion "the rank of any leaf 因此在插入所有的節點時 ,不論其出身(一視同仁的味道飄出來了),它使用來進行平衡的原理是利用紅黑樹要求所有的 leaf 到 root 所有的黑色節點都有一樣的數量,來進行平衡。
這邊我要收回我前面說的,
linux kernel 的程式碼直觀多了QQ
在這個過程中,我發現,雖然我對 `_RB_INSERT_COLOR` 還是沒有很了解,但是在`_RB_INSERT_FINISH` 以及其他函式,沒有很多的 node swap 操作。
接下來比對一下 FreeBSD 之餘 Linux 並且試著找出為何會在一些情況下優於其的因素。
也許問題不出在這邊,但我們先觀察一下。
我著重比較的地方為 `rb_insert_color`
因為我發現, linux 與 FreeBSD 對於判斷祖輩節點的顏色,判斷當前節點位於的位置,等等方式有所不同,
我先統整一下。
### how Linux red_black_tree works
### 對比
Linux:
`rb_set_parent_color`
```c
static inline void rb_set_parent_color(struct rb_node *rb,
struct rb_node *p, int color)
{
rb->__rb_parent_color = (unsigned long)p | color;
}
```
FreeBSD:
`_RB_INSERT_COLOR`
我們可以看到, FreeBSD 用來判斷插入節點對於樹高影響的方式為:
這邊利用 RB_BITS(gpar(有可能為1.2.3)) 來對 elmdir(判斷是在 parent 節點的左邊還是右邊) 做判斷,
判斷的重點為將兩者做 bitwise-AND 操作
#### 旋轉(rotation)
老師上課提供的教材有提到,
在紅黑樹操作中,旋轉是需要教高成本的操作,
因此我需要判斷兩個東西,
第一個是 linux 以及 FreeBSD 做一個旋轉所需的成本,
並且分析他們在各個情況下進行旋轉的次數。
FreeBSD:
```c
#define RB_ROTATE(elm, tmp, dir, field) do { \
if ((_RB_LINK(elm, dir ^ _RB_LR, field) = \
_RB_LINK(tmp, dir, field)) != NULL) \
RB_SET_PARENT(_RB_LINK(tmp, dir, field), elm, field); \
_RB_LINK(tmp, dir, field) = (elm); \
RB_SET_PARENT(elm, tmp, field); \
} while (/*CONSTCOND*/ 0)
```
插入`_RB_INSERT_COLOR`:
這邊被 vscode 坑了一把,註解上面的圖片跑板了...因此在詳細閱讀完以後,我重新復原了它
not valid case1:
```
parent parent
/ \ / \
element n1 ----> child n1
/ \ / \
n2 child element n4
/ \ / \
n3 n4 n2 n3
```
not valid case2:
```
parent child
/ \ / \
child n1 ----> n2 parent
/ \ / \
n2 n3 n3 n1
```
**依據我的理解,每次 call 這個函式最多會呼叫兩次 rotation 功能**
刪除`_RB_REMOVE_COLOR`:
case1:
```
parent parent
/ \ / \
element sibline -----> element element*
/ \ / \
element* n1 n2 sibline
/ \ / \
n2 n3 n3 n1
```
case2:
```
parent element
/ \ / \
n1 element -----> parent n3
/ \ / \
n2 n3 n1 n2
```
這邊我們不能分開看,
要將兩個情況做結合,
當一顆不平衡的樹進入這個函式中時,我們會需要將述作最後的平衡,在最差的情況下,
我們需要額外多做一次 case 1,
最後將 sibline 這棵子樹提取上來。
閱讀〈[Linux 核心的紅黑樹](https://hackmd.io/@sysprog/linux-rbtree)〉相關的例子後,我發現 Linux 有一個寫法跟 FreeBSD 不同。
接下來我們進行單獨討論。
### remove
remove的情況跟前者差不多
### 遇到的問題
我目前遇到的問題是,我找不到兩者明顯的效能差異所在(在對比了兩者對於自平衡相關的操作,我還找不到顯著的效能差異)
目前只能看到結果,
但如果想要提昇 linux,或是題目上面的程式效率,
FreeBSD 版本的紅黑樹必定有值得學習參考的地方,
### 不同情境下的議題
我目前想要了解的是,在 FreeBSD 上的實作,能看到他是將盡量規避一些進入 `_RB_INSERT_FINISH` 之中的 `rb_insert_color` 而刪除亦然,
有跟老師討論中提到,FreeBSD 的寫法在於將一些需要左右旋轉的函式獨立出來,並且加入了 `spy_tree`,在一般的演算法書籍中有提到,這種樹的時間複雜度不甚理想(在數字體量上夠大時),但目前我們在 `RB_BRANCH` 這項專案之中,我們需要的場景需要的數字量不超過 128 個。
其他也很類似的場景中,我們不一定需要有多大的計算量,
因為 Jserv 老師有提到,紅黑樹對 cache 不是那麼的友好,
所以我需要探討出,在 128 數量以下的操作中,我們需要付出哪些代價來執行紅黑樹,
並且,在 freebsd 之中的實作,他有一些相對 linux 小的支出,
目前我的探討方向有三個點。
#### 1.找出為何 rb_tree 對 cache 不友善
在我查詢了很多篇 CSDN 上面的資源,都沒得到很滿意的答案,
但是我在 jemalloc 的 GitHub 上面看到 [issue #2043](https://github.com/jemalloc/jemalloc/issues/2043),提到一些關於為何 red-black tree 對於 cache 來說並不是特別友善的原因 cache miss ,甚至在討論中用到了 "terrible"
那為何會有 cache miss 呢?
以 Linux 核心為範例,當我們在判斷何時需要 ROTATE 時,
會需要讀取至少一個點的顏色,
~~而且不需要其他節點相關的資訊,~~
~~那這樣必定會遭遇一次 cache miss,~~(沒有什麼一定不一定...)
經過我的查證與理解,我發現我錯了。
紅黑樹產生 cache miss 是因為各個節點不但不連續存儲在一起,並且使用了很多的間接指標。
這樣的情況下更容易造成所需的東西並不在 cache 之中,
我們來看看 Linux 核心之中相關的案例,
如同以下幾個案例:
`tmp = gparent->rb_right;`
在 kernel 之中,我們時常使用到下面這個函式,但是它對 cache 並不是那麼的友好,
因為據我的想法,它涉及到了寫入的操作,
在一些情況下會有可能造成我們在 cache 讀取時發生 miss (這邊說發生錯誤我認為並不嚴謹)。
```c
static inline void rb_set_parent_color(struct rb_node *rb,
struct rb_node *p, int color)
{
rb->__rb_parent_color = (unsigned long)p | color;
}
```
但是其中的 element 資料我們其實不需要,但是我們有可能會遇到 cache miss ,花費了額外的時間與資源來進行查找出一段我們不需要的資訊。
我要來比較兩者之間的優劣,
#### FreeBSD:
插入:
這邊可以藉由上述部份程式碼看到,我們的 `rb_insert` 並不是像 Linux 一樣,逐步執行下去,而世界由多個函數來組成,盡可能的避免一些會造成 cache missing 的情況。
刪除:
todo...
#### Linux:
插入:
我們需要一直執行 `__rb_insert` 裡面有一個 while loop ,直到我們給定的 parent 為黑色(進入 `!parent` 的情況),
在這個 while loop 中,
雖然不是每一個場景下都需要進行到 case 1~case 3
(這邊不另行解釋, chatGPT 逐行解說比我強多了),
但是我發現,在這過程中,
我們需要不停的重複指派 `tmp` 還有 `gpar` ,並且每次變更內容時,都是用新的節點來當做指定值,這樣很有可能會造成我們的 cache miss 問題。
刪除:
刪除亦然,並且更糟糕的是,我們會需要不停的去檢查刪去後各個節點的情況,這是必會需要更頻繁的指定不同的值給我們的 parent 或是 gparent。
#### 2.splay_tree 為何會出現在 FreeBSD 的紅黑樹實作之中?
觀察到當最近被存取的節點很可能在未來再次被存取時,[splay tree](https://en.wikipedia.org/wiki/Splay_tree) 的表現會優於紅黑樹,並且對於需要快速查詢的應用,其效能也相當不錯。其操作方式可以參考〈[Splay tree 圖解與實作](https://blog.csdn.net/u014634338/article/details/49586689)〉。
儘管根據教科書《Fundamentals of Data Structures in C》的描述,這種樹的效能相對較差,但在以上情況下,它確實具有某些優勢。接下來,我將分析其操作方式和旋轉方式,並試著將其與我們先前提到的三種不同實現紅黑樹進行比較。
特點:建立在 locality 之上
其時間複雜度為 $O(m \times \log n)$,$m$ 即是操作次數,當我們操作 $m$ 次,可以保證每次都是 $O(logn)$
原理為:
在每次的操作之後,都會將剛存取到的節點旋轉到根節點,為的是要加速我們後續的操作,具體加速的理論是,當需要重複存取某些節點時,他們會很靠近我們的樹根,進而減少存取所需的時間,並且,一些會需要被頻繁存取的節點,會被靠攏在樹根附近,減少需要旋轉的次數,也可以節省一些成本。
**提問:說到底,它這樣也是為了減少 cache miss 嘛?**
旋轉:
以下圖片摘自上篇提及的 CSDN 文章
左旋
![](https://i.imgur.com/TiEKslh.png)
右旋
![](https://i.imgur.com/dbQ4yT2.png)
伸展:逐層 vs 雙層
逐層:
當某個 `node v` 被訪問後,通過左旋以及右旋,使其上升至 root (這邊我的理解是,當我們存取某個節點時,它會逐步的靠近我們的 `root`,有利我們後續的存取)
雙層:
由於上述的方法雖然簡單,但是有一個致命的問題,存取時採用逐層伸展的策略,最壞情況為$O(n)$,這樣會使我們的時間複雜度變為$O(mn)$,這是我們不樂見的,
因此開發者做出了一個改良過的方法,
重點為每次操作時向上調整兩層,
具體方式展示如下
![](https://i.imgur.com/eXDkILe.png)
這個方法的優點為,它可以有效的減少整棵樹的高度,
這也是一個很好的方法來減少左傾樹的高度(在完全左傾的情況下,可以直接使高度減半!)
從這個樹(高度為15)
![](https://i.imgur.com/4yzTVUj.png)
轉換為(高度為7)
![](https://i.imgur.com/ROGSNF8.png)
缺點:
這種樹的缺點也是十分明顯的,
第一,它不是一個嚴格的自平衡樹。
第二,即使它只有唯讀情況下,自身結構仍然有可能會被改變,並且這個過程是很吃資源的(畢竟我們一直在上面討論怎麼不要進行旋轉,以免造成 cache miss 等等麻煩)。
第三,他有可能會成為一條 linked list ,這個情況下毫無優勢可言,並且會造成這案例的情形是很常見的,只要在有順序的(原文說是當 sequence 的方式進行)情形下輸入數組便會造成!
#### 3.FreeBSD 為何需要將一些函適合外打包出來而非如同 linux 的作法。
進一步觀察第一點,我們可發現之所以這樣做,可能是為了減少讀取親代節點的次數,進而減少 cache miss,在特定情況下形成 cache-friendly 的資料結構。然而,這裡存在一個問題:在 GitHub 上的 issue 討論串和 jemalloc 的官方文件中宣稱此方式是對 cache 友善的,但是要如何定義一個資料結構是 cache-friendly 呢?實際上,即使使用此方式,仍然無法避免許多 cache miss。
> 參見 [Red-black trees are pretty much terrible for performance](https://news.ycombinator.com/item?id=13263664)
#### 效能比較與測量
我在使用 make images 之後,發現只有 .png 檔案有被 modify,但是 make all 並沒有實際更新我們的 XML 檔案,目前正在解決相關問題,
這邊遇到一個問題卡了,我在寫完 `test.h` 想要進行 `make inti` 時,會遇到一些錯誤訊息,這邊 debug 了很久...
```
undefined reference to `test_rbtree_llrb'
...
...
```
這邊與老師討論到一些不同的情境,我需要修改一些測量方法
像是非連續性插入或是刪除,加入更多的測量數字體量等等...
### 如何使用 rb_bench
閱讀 `test.h`
我改寫了部分內容
先講一下老師提供的使用方法,以免出現跟我一樣蠢的失誤(以下),
:::warning
~~我發現無法使用 `test.h` 來產生 XML 裡面的數據,這邊我要好好處理一下。~~
:::
一定要執行以下步驟來使用我們的專案:
step1:
`make all`
step2:
`刪去 report/XXX.png`
step3:
執行`./rb-bench > reports/test-linux-emag.xml `(這個過程很久,慢慢等)
step4:
執行`./plot.py reports/test-linux-emag.xml reports/test-linux-emag.png`
一直 `make` 是沒有結果的( trace code 功力好弱...),
像是原始情況使用 128 我們就會得到 128 個數據在每一組,
在我的版本之中,
128
![](https://i.imgur.com/lLXXNIp.png)
256
![](https://i.imgur.com/xR2GTwp.png)
512(大概跑了30分鐘)
這邊可以看到,噪音變得非常多...
老師有說要優化這個問題,需要忽略我們的 outlier
![](https://i.imgur.com/Y43xoDo.png)
1024(天荒地老...)
![](https://i.imgur.com/9jUUPn3.png)
可以看到我們遇到了跟作業0一樣的情況,當數量被放很大時(其實也還好),有一些需要額外刪去的因素,會影響我們測試的準確性
這邊出現了一個很有趣的現象,當我們的測試數量往上提升之後,Linux 竟然有較好的性能(跟 FreeBSD 相比的話)
為何呢?
### 非連續插入以及刪除
那個...我使用了順序插入並且隨機再次插入,結果我們可以看到一個很神奇的圖片
![](https://i.imgur.com/YSvwhgs.png)
修改一下 `plot.py` 使得我們的結果更好看。
很明顯這個修改並沒有什麼意義
我們只能試圖從中看出,