研讀 box64 的紅黑樹實作
問題:
1. 紅黑樹在 box64 專案中的作用為何?
2. 為何一定要使用紅黑樹?
3. 我能如何改進呢?
## 基本紅黑樹的特性
1. 所有節點非黑即紅
2. 根節點為黑
3. 葉子節點為黑
4. 紅色節點的子節點必為黑色節點
5. 對於每一個節點,所有通往其後代葉子節點的路徑上必含一樣數量的黑色節點
## box64 中的紅黑樹
>The main functionality of the red-black trees is to provide a fast, cheap way to map ranges of addresses to a number and acccessing by providing an address in this range. [#2039](https://github.com/ptitSeb/box64/issues/2039#issuecomment-2485419771)
紅黑樹實作來自 2023 年 12 月 31 日合併的 [#1180](https://github.com/ptitSeb/box64/pull/1180) (即 commit [5e9e1faedc97194e46f3fb4b3665ec416ce7efbf](https://github.com/ptitSeb/box64/commit/5e9e1faedc97194e46f3fb4b3665ec416ce7efbf))
> This PR switches the way memory permission is tracked, from a sparse array to a red-black tree (self-balancing binary search tree). The notion of hot pages has also been removed.
>
> Performance does not seem to be very impacted, and memory usage has decreased a bit for processes which used a lot of RAM (for example, Steam uses ~100M less memory and every process of wine uses ~15M less memory).
藉由 `git checkout 5e9e1faedc97194e46f3fb4b3665ec416ce7efbf^` 可以切換到引入紅黑樹之前的版本,可以注意到在早期的版本所有記憶體有關的操作都在 `custommem.c` 之中,閱讀早期的 `custommem.c` 對理解 box64 的記憶體管理有巨大的幫助 。
而改用紅黑樹後,節省大量的記憶體,且效能衝擊不明顯(嗎?)。因此,倘若我們可提出效能略好但更省記憶體的方案 (如 jemalloc 的 rb,及後續 rv32emu 的 map),應可讓 box64 受益。
##### 節點的結構:
```c
typedef struct rbnode {
struct rbnode *left, *right, *parent;
uintptr_t start, end;
uint32_t data;
uint8_t meta;
} rbnode;
```
可以注意到在 box64 的紅黑樹節點紀錄了兩個資訊 : 整數 `data` 與 記憶體區段 ( `start` ~ `end`)。
如此一來便可以用較小的記憶體 (48) 來管理一大段記憶體 (e.g allocsize = 2097168)
`meta` 代表 `IS_LEFT` (0x1), `IS_BLACK` 或 `0`
利用 `sizeof` 可以觀察到節點的大小為 48 bytes 。
在標頭檔 [rbtree.h](https://github.com/ptitSeb/box64/blob/main/src/include/rbtree.h) 中發現在 box64 專案中定義了以下有關紅黑樹的操作:
```c
rbtree_t* rbtree_init(const char* name);
void rbtree_delete(rbtree_t* tree);
uint32_t rb_get(rbtree_t* tree, uintptr_t addr);
int rb_get_end(rbtree_t* tree, uintptr_t addr, uint32_t* val, uintptr_t* end);
int rb_set(rbtree_t* tree, uintptr_t start, uintptr_t end, uint32_t data);
int rb_unset(rbtree_t* tree, uintptr_t start, uintptr_t end);
uintptr_t rb_get_righter(rbtree_t* tree);
```
1. `rb_get` : 藉由 `addr` 找到其所屬的紅黑樹節點並回傳節點的 `data` 。
例如 `getProtection`, `getProtection_fast` 和 `getMmapped` 都是藉由 `rb_get` 的回傳值來判斷
1. `rb_get_end` :藉由 `addr` 找到其所屬的紅黑樹節點並利用指標寫入的方式獲取該節點的 `data` 以及記憶體區段的結尾 `end` 。
由於可以獲得某一記憶體區段的結尾 `end` , `rb_get_end` 可以用來走訪一大段連續的記憶體。
2. `rb_set` : 將記憶體區段 ( `start` ~ `end`) 與 `data` 加入紅黑樹,若新加入的記憶體片段與原有的儲存的記憶體片段有重疊,則重疊部分的 `data` 會被覆蓋成新的 `data`,如此一來便不會有重疊的記憶體片段。
3. `rb_unset` : 刪除記憶體區段 ( `start` ~ `end`)
4. `rb_get_righter` : 回傳指向紅黑樹 `tree` 的最右節點的指標,該節點代表的是整顆紅黑樹所涵蓋的記憶體範圍中地址最大的區段。
我們來看看 box64 紅黑樹的細節: ([rbtree.c](https://github.com/ptitSeb/box64/blob/main/src/tools/rbtree.c))
```c
int add_range(rbtree *tree, uintptr_t start, uintptr_t end, uint32_t data) {
rbnode *cur = tree->root, *prev = NULL;
while (cur) {
prev = cur;
if (cur->start < start) cur = cur->right;
else cur = cur->left;
}
return add_range_next_to(tree, prev, start, end, data);
}
```
這個函式的目標是藉由指標 `start` 找到節點 `prev` 並呼叫函式 `add_range_next_to`
而這個 `prev` 是什麼呢? 可以從註解 `// Make sure prev is either the rightmost node before start or the leftmost range after start` 得到提示:`prev` 是離 `start` 所指向的地址最近的節點 (結尾節點)
換句話說,box64 的紅黑樹與傳統二元搜尋樹不一樣,是以指定的地址而非 `data` 的數值作為插入的依據。
### 紅黑樹與記憶體管理
在 box64 中使用了四顆紅黑樹來做記憶體管理與追蹤,分別是:
- `memprot` : 紀錄一段記憶體的存取權限
- `mapallmem`:紀錄所有 memory mapping
- `mmapmem` : 紀錄由 mmap 所 map 出來的記憶體
- `blockstree`:紀錄一段 block 的記憶體,其 `data` 為該 block 在陣列 p_block 中的索引值
`mapallmem` 跟 `mmapmem` 的 `data` 有三種數值,分別為:
- `0` : 該段記憶體沒有被 mapped
- `1` : 該段記憶體被被 mapped 並紀錄在 `mapallmem`
- `2` : 該段記憶體被被 mmap mapped 並同時紀錄在 `mapallmem` 與 `mmapmem`
問題
1.由 map 所配置的記憶體為何要被特別紀錄?
2. 為何不統一紀錄在 `mapallmem` ,並用 `data` 來紀錄是否為 mmap 所配置的記憶體,畢竟使用 `rb_get` 就能檢查某段記憶體是否被紀錄在紅黑樹中
會用到 `mmapmem` 的函式只有
這時我們要分析 `setProtection` 與 `setProtection_mmap` 的差異,因為這是唯一一個 `mmapmem` 被建立的場景。
後者被使用的場景有 `src/tools/wine_tools.c` 和 `src/wrapped/wrappedlibc.c`
務必要釐清 `mmapmem` 與 `mapallmem` 的核心差異,方能合併兩者
```c
setProtection_mmap((uintptr_t)my_wine_reserve[idx].addr, my_wine_reserve[idx].size, 0);
```
## 追蹤紅黑樹操作
#### 1. 修改程式碼,加入 printf 追蹤以下動作:
- rb_set
- rb_unset
- rb_get
- rb_get_end
#### 2. [重新編譯 box64](https://hackmd.io/@lambert-wu/HJ6royNKR#:~:text=linux%2Dgnu%2Dgcc-,%E7%B7%A8%E8%AD%AF%20box64,-%E7%95%99%E8%A8%80)
但編譯時會遇到小問題:
```shell
/home/chiu/box64/src/tools/rbtree.c:728:93: note: 'stdout' is defined in header '<stdio.h>'; did you forget to '#include <stdio.h>'?
```
問題不大,加上`include <stdio.h>`即可
#### 3. 在 pc 端上用 qemu [測試](https://hackmd.io/Uqi1DK8VToup_UGi73r-Mw?both#:~:text=sysroot/lib/custom-,%E6%8E%A5%E8%91%97%E5%88%A9%E7%94%A8%20QEMU%20%E6%B8%AC%E8%A9%A6%3A,-%E7%95%99%E8%A8%80):
1. 將可執行檔 box64 改為 box64_mark 避免混淆,接著記得將其位置加入環境變數 PATH 中
2. 執行 `qemu-riscv64 -L sysroot box64_mark --help` 來檢查是否可以成功模擬 box64_mark
但此時會遇到錯誤:
```shell
qemu-riscv64: Could not open '/lib/ld-linux-riscv64-lp64d.so.1': No such file or directory
```
原來是之前使用 sysroot 時誤刪了重要的檔案 '/lib/ld-linux-riscv64-lp64d.so.1' ,重新下載 之後執行:
```shell
qemu-riscv64 -L Documents/riscv/sysroot box64/build_mark/box64_mark --help
```
就可以看到相關資訊
4. 執行 `box64_mark` 並統計輸出結果,以 `qsort_x86` 為例:
- rb_set: 223
- add_range: 35
- Adding: 73
- Removing: 20
- rb_unset: 21
- rb_get: 196
- rb_get_end: 220
- rb_get_righter 0
#### 4. 在 Tinker V 上測試:
>box64 的測試不能全在 QEMU 上進行,因為後者採用動態編譯,行為會不一致,因此需要在 Tinker V 實驗
問題:動態編譯為何會導致紅黑樹的行為不一致呢?
1. 在 pc 端編譯用於統計命令的 `count.c` 並將其傳輸至 `tk2`
1. 利用命令 `scp -O box64_mark tk2:/debian/usr/bin/` 將編譯好的 `box64_mark` 放入 `tk2` 上的`/debian/usr/bin`
2. 執行 `box64_mark [file name] | ./count` 並觀察輸出,以 qsort 為例:
| Operation | Count |
|-----------------|-------|
| rb_set | 192 |
| add_range | 4 |
| Adding | 74 |
| Removing | 4 |
| rb_unset | 1 |
| rb_get | 683 |
| rb_get_end | 499 |
| rb_get_righter | 0 |
可以發現有些程式在執行第二次後其命令數目便會固定,但有些程式的命令數目則始終保持恆定
4. 以下是各 bench 在執行第三次後的命令數量統計:
| bench | rb_set | Adding | Removing | rb_unset | rb_get | rb_get_end | rb_get_righter |
|----------------|--------|--------|----------|----------|--------|------------|----------------|
| qsort | 241 | 87 | 51 | 63 | 1090 | 753 | 3 |
| primes | 165 | 56 | 20 | 33 | 613 | 507 | 3 |
| dhrystone | 187 | 64 | 28 | 42 | 801 | 586 | 5 |
| aes | 208 | 72 | 36 | 47 | 978 | 648 | 5 |
| norx | 215 | 74 | 38 | 51 | 1042 | 678 | 3 |
| miniz | 486 | 196 | 152 | 147 | 2818 | 1535 | 11 |
| sha512 | 176 | 64 | 28 | 44 | 771 | 566 | 3 |
問題:
1. 是否有像是「清理快取」的功能以便觀察建立快取的情形?
2. 是否有必要觀察「建立快取」的過程?
#### 5. 對照 box64 的紅黑樹操作與 Mado 視窗操作
為了避免細微的動作對結果的影響,有關預測量之視窗事件的動作皆會重複多次:
| 視窗事件 | rb_set | rb_unset | rb_get | rb_get_end | rb_get_righter |
|------------|--------|----------|--------|------------|----------------|
| 未動作(僅關閉視窗) | 6261 | 332 | 37695 | 14404 | 6 |
| 拖曳視窗(沿著邊匡繞三圈)| 6568 | 342 | 45753 | 15068 | 6 |
| 滑鼠點擊(十次)| 6569 | 342 | 40242 | 15067 | 6 |
| 滑鼠移動(沿著邊匡繞三圈)| 6420 | 338 | 42496 | 14735 | 6 |
| 操作計算器(三次)| 6627 | 343 | 43870 | 15185 | 6 |
可以注意到 `rb_get` 與 `rb_get_end` 的數量皆是 「萬」級,若能精簡紅黑樹節點佔用的空間,使一條 cacheline 中能塞入更多節點,將對效能產生巨大的影響
### 將 box64 的紅黑樹程式碼抽出並獨立運作
目標:熟悉 box64 紅黑樹在 box64 記憶體操作流程中所扮演的角色,即以下三個函式中:
- `box64context.c` : rbtree_init, rbtree_delete
- `custommem.c` : findBlock, internal_customMalloc, internal_customMemAligned, FindDynablockFromNativeAddress, box32_dynarec_mmap, AllocDynarecMa, FreeDynarecMap, protectDBJumpTable, protectDB, unprotectDB, isprotectedDB, updateProtection, setProtection, setProtection_mmap, setProtection_elf, refreshProtection, allocProtection, freeProtection, getProtection, getProtection_fast, getMmapped, memExist, find31bitBlockNearHint, find47bitBlockNearHint, isBlockFree, reverveHigMem32, my_reserveHighMem, init_custommem_helper
- `dynablock.c` : InvalidDynablock, InvalidDynablock, internalDBGetBlock
為了方便測試,可將 `dynablock.c` 和 `box64context.c` 內用到的紅黑樹操作合併到 `custommem.c` 。
TODO:說明結構 `context` 的用途
TODO:box64 +wine 會用到 8200 個節點,深度為 14,遠遠於最大深度限制,但為何會造成錯誤
執行以下命令:
```shell
$ mkdir -p rbtree-test
$ cd rbtree-test
$ wget https://raw.githubusercontent.com/ptitSeb/box64/refs/heads/main/src/include/rbtree.h
$ wget https://raw.githubusercontent.com/ptitSeb/box64/refs/heads/main/src/tools/rbtree.c
```
修改 `rbtree.c` 的開頭數行:
```c
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include "rbtree.h"
#ifdef RBTREE_TEST
#define printf_log(...)
#define dynarec_log(...)
#define rbtreeMalloc malloc
#define rbtreeFree free
#else
#include "custommem.h"
#include "debug.h"
#define rbtreeMalloc customMalloc
#define rbtreeFree customFree
#endif
```
編譯和測試:
```shell
$ gcc -O2 -o rbtree -I. -D RBTREE_TEST rbtree.c
$ ./rbtree
```
參考執行輸出:
```
[0]
([1], (B/0x600000a042a0) 130000-140000: 7, [1])
([1], (B/0x600000a042a0) 130000-140000: 7, ([1], (R/0x600000a042d0) 141000-142000: 135, [1]))
([1], (B/0x600000a042a0) 130000-140000: 7, ([1], (R/0x600000a042d0) 140000-142000: 135, [1]))
([1], (B/0x600000a042a0) 130000-141000: 7, ([1], (R/0x600000a042d0) 141000-142000: 135, [1]))
([1], (B/0x600000a042a0) 130000-140000: 7, ([1], (R/0x600000a042d0) 140000-142000: 135, [1]))
0x141994 has attribute 135
```
可在此目錄執行 `git init ; git add *.[ch] ; git commit -a` 以將紅黑樹程式碼納入版本控制,便於後續實驗。
#### 使用 GDB 分析 box64 記憶體管理流程
以 `qsort_x86` 為例,分析紅黑樹相關操作的啟動流程
```shell
gdb --args box64 qsort_x86
```
### Hook Function
程式配置記憶體的頻繁程度和區域巔峰量
吞吐量的定義為單位時間所操作的記憶體大小
由於節點大小是固定的,所以計算方式就是 (節點數量*節點大小)/ 時間
建立紀錄資訊用的結構:
```c
typedef struct record_item{
const char* tree_name;
const char* op_name;
long tree_number;
long node_number;
struct tm *op_time;
}record_item;
```
TODO: 建立共享記憶體空間後在程式執行的過程中將資訊紀錄在 `record_item` 陣列中
#### 搭配 custommem.c 並設計測試情境
在開始測試前需要將 custommem.c 所需的檔案一併獨立出來
1. 配置 block
2. 刪除 block
3. 查看 block
4. 走訪 block
### 研讀紅黑樹實作改進和紅黑樹實作
#### 2-3-4 樹、紅黑樹與左傾紅黑樹
紅黑樹是實踐 2-3-4 樹的一種方式,紅黑樹的「紅色節點」代表該節點與其親代節點在對應的 2-3-4 樹中屬於同一節點。
這也是紅黑樹能要保證「任一根到葉子路線上的黑節點數量一致」的原因,因為 2-3-4 樹中任一根到葉子路線長度都是一樣的。
然而給定 2-3-4 樹可以對應到多個紅黑樹(但我不知道為什麼這是個問題),因為 3 node 可以對照到兩種紅黑樹:
```graphviz
digraph {
node [shape=record, fontcolor=black, fontsize=14, width=4.75, fixedsize=true];
values [label="<f0> 33 | <f1> 48 | <f2> 80 ", color=blue, fillcolor=lightblue, style=filled];
"35/50" [label="35/50", style=filled];
edge [color=blue];
"35/50":f0 -> values:f0 ;
"35/50":f1 -> values:f1 ;
"35/50":f2 -> values:f2 ;
"35/50":f5 -> values:f5 ;
}
```
該 3 node 可以對應成以下兩種紅黑樹 (`35` 為紅色子節點,代表 `35` 與其親代節點 `50` 在對應的 2-3-4 樹中屬於同一節點)
```graphviz
digraph RBT {
graph[ordering="out"]
node [shape=circle, fixedsize=true, style=filled,width=.5]
35 [fillcolor=red]
50 -> {35 80}
35 -> {33 48}
}
```
或是
```graphviz
digraph RBT {
graph[ordering="out"]
node [shape=circle, fixedsize=true, style=filled,width=.5]
35 -> {33 50}
50 -> {48 80}
50 [fillcolor=red]
}
```
為此,左傾紅黑樹([Left-leaning Red-Black Trees](https://sedgewick.io/wp-content/uploads/2022/03/2008-09LLRB.pdf))規範了 3 node 必須要 "left-leaning"

「左傾紅黑樹」與「紅黑樹」的差異在於前者增加了「若一節點只有一個紅色子節點,則該子節點必在左側」的特性,其餘五個特性相同。
只要掌握「左傾紅黑樹」能對應到唯一的「 2-3-4 樹」的特性,便可將紅黑樹操作對照到 「 2-3-4 樹」操作,如此一來在進行紅黑樹操作時只會遇到以下三種可類型(如下圖 A, B and C),複雜的操作就可以變的很直觀(相較於一般紅黑樹會有2+5種):
```graphviz
digraph RBT {
graph[ordering="out"]
node [shape=circle, fixedsize=true, style=filled,width=.5]
A -> {nil_ _nil_}
B -> {x nil}
x [fillcolor=red]
C -> {y z}
y [fillcolor=red]
z [fillcolor=red]
}
```
##### 插入:
原先的紅黑樹插入要判斷八種情況(兩組四種),若使用左傾紅黑樹則可以簡化成三種情況:
1. 欲插入的親代節點沒有其他子節點(`A`):
此舉相當於對 2 node 加入一個節點使其變成 3 node (`B`) ,不需額外調整
2. 欲插入的親代節點有一個子節點(`B`):
此舉相當於對 3 node 加入一個節點使其變成 4 node (`C`) ,這時要考慮以下兩種情形:
- 加入的節點為三者最大,則該節點為右子節點,不需額外調整
- 加入的節點為三者最小,相當於連續兩個紅節點,需作旋轉
4. 欲插入的親代節點有兩個子節點(`C`):
此舉相當於對 4 node 加入一個節點,此時會節點兩端會變成兩個 2 node (`B`),而中間的節點會變成兩個 2 node 的根節點,接著再將節點加入匹配的 2 node 中使其成為 3 node ,方法跟情況 1 一樣。
此步驟看似複雜,但若切換到紅黑樹的視角儘僅是將顏色交換後再插入節點(如下圖所示,欲加入節點 F )
```graphviz
digraph RBT {
graph[ordering="out"]
node [shape=circle, fixedsize=true, style=filled,width=.5]
C -> {y z}
y [fillcolor=red]
z [fillcolor=red]
F
y -> {nil nil_}
z -> {_nil _nil_}
}
```
```graphviz
digraph RBT {
graph[ordering="out"]
node [shape=circle, fixedsize=true, style=filled,width=.5]
C -> {y z}
C [fillcolor=red]
z -> {F nil}
F [fillcolor=red]
y -> {_xnil nil_}
}
```
當然,以上 1, 2, 3 都是遞迴操作,確保整棵樹都能維持紅黑樹的特性
##### 刪除:
#### jemalloc
>在 jemalloc 中,不使用 parent pointers,而是在進行插入或刪除時才會用 path 陣列去記錄走過的路徑,直接減少了每個節點結構體的大小,在 cacheline 固定為 64 bytes 的情況下,可以使一次能放入 cache 的節點數量增加,提升整體系統的使用效率。
path 的機制?
#### 研讀〈[Left-Leaning Red-Black Trees Considered Harmful](https://read.seas.harvard.edu/~kohler/notes/llrb.html)〉
Sedgewick’s left-leaning red-black
### jserv/rbtree
TODO: 加上摘要 並說明 malloc vs. alloca
#### `rb_t` :
```c
typedef struct {
rb_node_t *root; /**< Root node of the tree */
rb_cmp_t cmp_func; /**< Comparison function for nodes */
int max_depth;
#if _RB_DISABLE_ALLOCA != 0
rb_node_t *iter_stack[_RB_MAX_TREE_DEPTH];
bool iter_left[_RB_MAX_TREE_DEPTH];
#endif
} rb_t;
```
- `root` :指向根節點的指標
- `cmp_func` :
- 堆疊:用於紀錄紅黑樹上某一段路徑上的所有節點,由兩種配置方式
- 靜態配置:
```c
rb_node_t *iter_stack[_RB_MAX_TREE_DEPTH];
bool iter_left[_RB_MAX_TREE_DEPTH];
```
其中 `_RB_MAX_TREE_DEPTH` 會根據執行環境的架構有所差異(TODO:補算式)
- 動態配置(問題:這樣算動態嗎?): 利用整數 `max_depth` 紀錄目前紅黑樹最深的路徑長,再根據此長度配置堆疊。
#### `rb_node_t` :
```c
typedef struct __rb_node {
struct __rb_node *children[2];
} rb_node_t;
```
可以注意到節點只包含了兩個指向子結點的指標,並不包含指向親代節點的指標與顏色,其大小 (64位元系統)為 16 byte。
##### color bit
節點的顏色紀錄在指向左節點的指標的 LSB
利用 `& ~1UL` 對指向左子節點的指標進行 masking (消掉 LSB) ,就可以得到真正的指標。
這樣的結構可以方便嵌入使用者自訂的結構,以 box64 為例,紅黑樹節點 node 就可以定義為:
```c
typedef struct rbnode {
rb_node_t node;
uintptr_t start, end;
uint32_t data;
} rbnode;
```
#### `rb_foreach_t`
```c
typedef struct {
rb_node_t **stack; /**< Hold the nodes encountered during traversal */
bool *is_left; /**< Track the relationship of each node to its parent */
int32_t top; /**< Keeps track of the current position in the stack */
} rb_foreach_t;
```
- `stack` 紀錄了某一段路徑上的所有節點
- `is_left` 紀錄了某一路徑上所有節點是否為其親代節點的左子節點
- `top` 紀錄當前節點 `node` 的索引值,同理 `stack[top-1]` 則為 `node` 的親代節點。注意 `top` 為 $-1$ 時代表堆疊尚未建立或初始化(記載之前的資料尚未清洗)。
TODO:說明如何找到同輩節點?
配置 `rb_foreach_t` 有兩種方式:
1. 靜態配置:直接使用結構體 `rb_t` 中的兩個陣列作為紀錄路徑的堆疊
```c
stack = &(tree)->iter_stack[0];
is_left = &(tree)->iter_left[0];
```
2. 動態配置:利用 `alloca` 配置記憶體
```c
stack = alloca((tree)->max_depth * sizeof(rb_node_t *));
is_left = alloca((tree)->max_depth * sizeof(bool));
```
#### rb_insert
此函式的功能是將節點 `node` 加入紅黑樹 `tree` 之中,過程包含尋找`node` 欲加入的位置和調整紅黑樹結構。
在走訪紅黑樹 `tree` 之前要先建立堆疊 `stack` 來紀錄從根節點到 `node` 的路徑。
建立堆疊有兩種模式:
1. fixed stack,使用 `rb_t` 結構中的堆疊(TODO:說明重複利用的情況)
```c
rb_node_t **stack = &tree->iter_stack[0];
```
2. (問題:這不也叫 fixed stack 嗎?),使用 `rb_t` 中的 `max_depth` ,設定成 `max_depth + 1` 是因為一次 `rb_insert` 最多加入一個節點,最大深度只能可是過去的最大深度 `max_depth` 再多一層。
```c
rb_node_t *stack = stack[tree->max_depth+1];
```
##### find_and_stack
此函數會利用二元搜尋樹的特性找到 `node` 或是 `node` 適合的位置(即符合二元搜尋樹的定義的位置),在尋找的過程中,走訪過的節點皆會被紀錄在堆疊 `stack` 中並記錄數量,最後回傳 `sz` 即為堆疊的深度。
```c
static int find_and_stack(rb_t *tree, rb_node_t *node, rb_node_t **stack)
{
int sz = 0;
stack[sz++] = tree->root;
while (stack[sz - 1] != node) {
rb_side_t side =
tree->cmp_func(node, stack[sz - 1]) ? RB_LEFT : RB_RIGHT;
rb_node_t *ch = get_child(stack[sz - 1], side);
if (!ch)
break;
stack[sz++] = ch;
}
return sz;
}
```
此時 `node` 還未被加入紅黑樹 `tree` 中,但透過剛才的走訪我們可以得到 `node` 未來的親代節點 `parent` (即 `stack[sz-1]`) ,注意 `stack[sz]` 此時還未建立,因為此時 `node` 還未加入 `tree` 中,無法透過 `get_child` 獲得。
找到 insertion point 後...(補細節)便可以把 `node` 加入 `stack` 中,此時的 `stack` 就是完整的路徑,有了此路徑便可以開始調整 `tree` 的結構以符合紅黑樹的定義。
##### fix_extra_red
```c
static void fix_extra_red(rb_node_t **stack, int stacksz)
{
while (stacksz > 1) {
const rb_node_t *node = stack[stacksz - 1];
rb_node_t *parent = stack[stacksz - 2];
/* If the parent is black, the tree is already balanced. */
if (is_black(parent))
return;
rb_node_t *grandparent = stack[stacksz - 3];
rb_side_t side = get_side(grandparent, parent);
rb_node_t *aunt =
get_child(grandparent, (side == RB_LEFT) ? RB_RIGHT : RB_LEFT);
/* Case 1: The aunt is red. Recolor and move up the tree. */
if (aunt && is_red(aunt)) {
set_red(grandparent);
set_black(parent);
set_black(aunt);
/* The grandparent node was colored red, may having a red parent.
* Continue iterating to fix the tree from this point.
*/
stacksz -= 2;
continue;
}
/* Case 2: The aunt is black. Perform rotations to restore balance. */
rb_side_t parent_side = get_side(parent, node);
/* If the node is on the opposite side of the parent, perform a rotation
* to align it with the grandparent's side.
*/
if (parent_side != side)
rotate(stack, stacksz);
/* Rotate the grandparent with the parent and swap their colors. */
rotate(stack, stacksz - 1);
set_black(stack[stacksz - 3]);
set_red(stack[stacksz - 2]);
return;
}
/* If the loop exits, the node has become the root, which must be black. */
set_black(stack[0]);
}
```
接著要更新 `rb_t` 結構中的 `max_depth` ,使 `max_depth` 能記載目前最長的路徑。
##### rotate
### rb_remove
首先我們要回顧中序走訪的定義:中序走訪(In-Order Traversal)是依序以左節點、根節點、右節點為順序走訪的方式。
因此找到「相對於某一節點的最左節點」變得至關重要,因為該節點即為某一抽象單元的走訪起點(TODO:換個更好的方式表達)

以上圖的例子來說....(待續)
### stack_left_limb
```c
static rb_node_t *stack_left_limb(rb_node_t *n, rb_foreach_t *f)
{
f->top++;
f->stack[f->top] = n;
f->is_left[f->top] = false;
for (n = get_child(n, RB_LEFT); n; n = get_child(n, RB_LEFT)) {
f->top++;
f->stack[f->top] = n;
f->is_left[f->top] = true;
}
return f->stack[f->top];
}
```
此函式的功能是將節點 `n` 以及其所有左子孫加入結構 `rb_foreach_t` 之中,函式預設 `n` 是根節點或是右子節點,這個預設在(?)得到保證。當 `n` 被放入節點並記錄 `n` 與其親代的關係後,函式隨即將其左子孫們(如果存在的話)加入堆疊並回傳 `n` 的最左子孫(TODO:確認是否有其他更精確的名詞)或是 `n` 。
TODO:示意圖
#### `__rb_foreach_next`
首先先將根節點到最左節點的路徑紀錄在堆疊中,做為之後操作的主要路徑
```c
if (!tree->root)
return NULL;
if (f->top == -1)
return stack_left_limb(tree->root, f);
```
為了實踐紅黑樹的中序走訪,必須依據當前節點 `f->stack[f->top]` 的情況分成以下幾種處理方式:
1. 當前節點擁有右子節點 `n` : 利用 `stack_left_limb` 找出 `n` 的最右子節點並回傳
```c
rb_node_t *n = get_child(f->stack[f->top], RB_RIGHT);
if (n)
return stack_left_limb(n, f);
```
2. 當前節點為左子節點且沒有右子節點(子樹的極值?)此時代表該節點已經被回傳過了(即該節點為某一路徑的終點,已被回傳過了),因此需要 pop `stack` 並回傳該節點的親代節點 `f->stack[--f->top]`。
(此處)
```c
if (f->is_left[f->top])
return f->stack[--f->top];
```
```c
while ((f->top > 0) && (f->is_left[f->top] == false))
f->top--;
f->top--;
return (f->top >= 0) ? f->stack[f->top] : NULL;
```
### 研讀〈Rank-Balanced Trees〉論文
該論文第一部分先介紹了三種自平衡的搜尋樹 (Red black trees、AVL tree and B-tree)
接著介紹一種抽象化的方式 Ranks:
此方式將節點中用來維持平衡的資訊皆抽象化為 `rank` ,而不同都自平衡樹會利用不同的 rank rule 來維持樹的平衡。
以下是幾個跟 `rank` 有關的操作:
- r(x) : Returns rank of a node
- parent(x) : Returns parent of a node
- rank difference of x : r(parent(x)) - r(x)
- x is an i-child : its rank difference is i
- x is an (i, j)-node : its left and right children have i and j rank differences respectively
接著示範在利用 rank 將不同自平衡樹抽象化後的結果:
1. AVL tree : Every node is (1, 1) or (1, 2).
In the case of the AVL tree, rank represents height.
(1, 2)-node represents a tree where one of its subtrees has a
bigger height. In this case, we are talking about the nodes with
balance factor −1 or 1 (respectively being signed with a − or a
+).
>問題:此處我看不懂,如果説 rank 是指節點高度的話那親代節點與子代節點的 rank difference 必為 1 ,換句話說,所以有的節點都會是 (1,1) ,如果無法搞清楚該論文的抽象化方法 rank 將無法看懂後面的分析
2. Red-Black Rule: All rank differences are 0 or 1, and no parent of a 0-child is a 0-child.
#### Week AVL tree
#### 評比程式
### 修改 box64 的紅黑樹實作
>參考 [Improve red-black tree implementation in map](https://github.com/sysprog21/rv32emu/issues/29)
整合現有實作 [jserv/rbtree](https://github.com/jserv/rbtree) ,該實作有以下特性:
- 使用「侵入」技法 (intrusive `rb_node_t` structure) -> 少了一層的間接操作,並有更好的 cache locality
- 移除親代指標,只有在「加入」及「刪除」時會紀錄在紅黑樹結構的 stack 中 -> 減少節點大小
- 使用 Indirect pointers -> 減少分支數量
- 使用指向右子節點的指標的 LSB 來紀錄節點顏色 -> 減少 node 大小
TODO:畫出 box64 的紅黑樹函式架構圖,並對對映到 jserv/rbtree 的函式,避免瞎忙
TODO :實作細節
> 必須善用「侵入式」的特性,無腦瞎改就無法獲得本應獲得的效能
1. 比較函式:
```c
bool compare(const rb_node_t *a, const rb_node_t *b){
rbnode* A = container_of(a, rbnode, node);
rbnode* B = container_of(b, rbnode, node);
if(A->start < B->start) return true;
return false;
}
```
2. 紅黑樹節點外的 container 結構:
```c
typedef struct rbnode {
rb_node_t node;
uintptr_t start, end;
uint32_t data;
} rbnode;
```
3. Search
4. Insert
5. Delete
TODO:
`add_range_next_to` 的功能是將代表一個記憶體區段的 `node` 加入紅黑樹成為 `prev` 的子節點。
而`add_range` 的功能是找到與欲加入之記憶體區段(`node`)最接近的節點 `prev`
之後呼叫 `add_range_next_to` ,因此上述功能可以完全由 `rb_insert` 來代替(`add_range` 前半段對應到 `find_and_stack` )
但 `add_range_next_to` 可以由 `rb_insert` 來替代嗎?
要釐清 `find_and_stack` 找到的 parent 是否用相等於 `add_range_next_to` 的參數 `prev`
TODO: 務必從 rb_set 開始改,先改枝微末節的小程式會陷入死胡同
TODO: 尋找前驅和後繼節點的時候需要用到親代節點的指標,要想辦法搞定
---
### TODO
設定改進紅黑樹實作的目標:
1. 加速插入/刪除/查詢 (符合 box64 使用情境)
2. 更低的記憶體開銷
- [x] TODO: 在 box64 程式碼增加紅黑樹新增/刪除/查詢的動作追蹤 (用 printf 或 [log.c](https://github.com/rxi/log.c)),以利後續效能評估。應建立圖表,使用固定的 x86-64 效能評比程式。注意:選定的[效能評比程式](https://hackmd.io/@devaraja/box64-bench)規模過小,不足以反映 box64 實際的應用場景,待程式碼充分改進後,可參照上述 git log,
- [ ] 利用 [box64 + Win64](https://github.com/ptitSeb/box64/blob/main/docs/X64WINE.md) 來執行經典 Windows 內建程式,以彰顯記憶體管理的開銷。
- [ ] TODO: 現行 [rbtree.c](https://github.com/ptitSeb/box64/blob/main/src/tools/rbtree.c) 的測試情境過於單純,不足以反映紅黑樹特性和 box64 使用案例,可整理之前的動作追蹤,將紅黑樹相關函式呼叫時的參數予以重現,作為測試案例的改進。如此一來,後續的程式碼改進就有一致的測試基準。
- [ ] TODO: `rb_get_end` 是 box64 特有的實作,且用於 `custommem.c` 作為記憶體管理機制,其實作為:
```c
int rb_get_end(rbtree_t* tree, uintptr_t addr, uint32_t* val, uintptr_t* end) {
rbnode *node = tree->root, *next = NULL;
while (node) {
if ((node->start <= addr) && (node->end > addr)) {
*val = node->data;
*end = node->end;
return 1;
}
if (node->end <= addr) {
node = node->right;
} else {
next = node;
node = node->left;
}
}
*val = 0;
if (next) {
*end = next->start;
} else {
*end = (uintptr_t)-1;
}
return 0;
}
```
倘若最後的節點不常更動,可比照 Linux 核心的 rbtree,快取最左邊的節點的作法 (參見 Linux CPU scheduler 書籍第三章),讓原本 $O(\log{n})$ 的時間複雜度降到 $O(1)$
- [ ] TODO: 研讀[紅黑樹實作改進](https://hackmd.io/@sysprog/ryDD1HgS3)和[紅黑樹實作](https://hackmd.io/@sysprog/HkZcqus8R)並留意 jemalloc (rv32emu 的紅黑樹改寫自 jemalloc 的 rb) vs. Linux 的實作。注意:jemalloc 的 rb 可能具備較低的記憶體開銷。
- [ ] TODO: 學習 [Improve red-black tree implementation in map](https://github.com/sysprog21/rv32emu/issues/29) 和 [rbtree_bench](https://github.com/EagleTw/rbtree_bench) 的手法,對 box64 的紅黑樹實作予以分析,可將後者獨立出來並包裝為一致的介面
- [ ] TODO: 查看 [alloca](https://man7.org/linux/man-pages/man3/alloca.3.html)
- [ ] TODO: 釐清 box64 中 `rb_` 系列函式的應用場景,整理並修改程式碼來觀察,倘若有疑慮,建立對應的 GitHub issue,請開發者澄清,務必先列出你的認知和推論,接著才提問
- [ ] TODO: 研讀〈[Rank-Balanced Trees](https://is.muni.cz/th/elt9h/thesis.pdf)〉論文 (2022 年)
- [ ] TODO: 針對 [jserv/rbtree](https://github.com/jserv/rbtree) 程式碼,提出改進現有效能評比程式的規劃,附上你預計利用 Python 的 Matplotlib 如何製作對應紅黑樹操作的視覺化
---
## 參考文獻
* [Faster, Simpler Red-Black Trees](https://www.khoury.northeastern.edu/~camoy/pub/red-black-tree.pdf)
* [A STL compliant map and set that uses a red-black tree under the hood](https://github.com/drogalis/Flat-Map-RB-Tree)