lab0-c
contributed by < willwillhi1
>
node_t
結構體參考作業一的 list_head
的實作,將原本的結構體中用來表示紅黑樹的地方抽離出來單獨使用。
原本的實作為以下,將 uintptr_t color
, struct __node *left, *right
從 node_t
裡移出來。
使用一致的程式碼縮排方式,亦即 4 個空白 (而非 tab)。
已修正
抽離出來用來表示紅黑樹的結構體定義為 rb_node
:
而原本的 node_t
結構體會變成:
其中裡面的 list_head
指標指向 element_t
結構體裡面的 list_head
,element_t
用來表示佇列的資訊,詳細的教學可以參考 你所不知道的C 語言: linked list 和非連續記憶體。
最後對用來表示整個 map
資訊的 cmap_internal
做一些小修改 :
node_t *head
改成 struct rb_node *head
,表示整顆紅黑樹的 root
。cmap_iter_t it_end, it_most, it_least
這個部份還在思考要改成 element_t
或是 list_head
或 struct rb_node
。保留 cmap_iter_t
的風格,作為走訪節點的通用 iterator
使用
struct *rb_node
可以作為走訪節點的通用 iterator
comparator
的部份,因為 element_t
的 value
的型別是字串,所以先改成以下作為替代,其中 _CMP_LESS
, _CMP_GREATER
, _CMP_EQUAL
分別代表 -1, 1, 0。建立節點的部份修改為以下,傳入的參數為 element_t
結構體裡的 list_head
的位址,然後 cmap_create_node
將紅黑樹節點初始化(將節點的左、右,及親子節點初始化為 NULL
,顏色標成紅色)。
在 tree_sort
最後執行,負責釋放整顆樹所用到的空間,目前的想法是用 DFS
的方式來走訪所有節點並釋放記憶體。
用來在開發時查看整個樹的架構,方便除錯
目前是採用時間複雜度 的方法
邏輯是依照樹高來印出該層的所有點,並用遞迴的方式來走訪所有的階層。
print_current_level
負責印出第 i
層所有點。
基本的 cmap_rotate_left
, cmap_rotate_right
和 平衡 LL
, LR
, RR
, RL
情況對應的操作 cmap_l_l
, cmap_l_r
, cmap_r_r
, cmap_r_l
做的修改只有把結構體 node_t
改成新定義的 rb_node
。
透過走訪整棵紅黑樹來重新計算整個佇列中的最大與最小值
最大就是從 root
開始一直往右找,反之最小則是往左。
測試結果
在原本的實作中有三個參數:
cmap_t obj
: 代表整個 map
的資訊。node_t *node
: 改為要插入的 element_t
的 list_head
位址。void *value
: 這裡在原本實作中是可以改進的,目前不會處理碰撞發生的情況,如果發生就會跳錯 not support repetitive value
。container_of
取得 node_t
的位址再用它的 list_head *list
成員去存取 element_t
中的 value
。tree_sort
tree sort
分成兩個部份,分別是建立紅黑樹然後在透過走訪樹來將佇列排序。
list.h
裡的 list_for_each
來走訪整個佇列,同時建立紅黑樹的節點。cmap_first
會回傳紅黑樹中位置最左的節點,也就是佇列中 value
最小的點。cmap_next
會找到該點在樹中的下一個順位的點,會分成兩個情況:
root
為止。下面程式中的 for
迴圈用 cmap_next
依序走訪整棵樹,然後用 list_del
將該點從佇列中移除後再用 list_add
加入。
最後釋放記憶體
qtest
修改在 qtest
中新增 do_tree_sort
函式。
最後在 console_init
函式中加入 tree_sort
命令。
TODO: 引入 option,讓 qtest
得以在執行時期切換不同的排序實作,有助於後續分析。
引入 option
首先在 q_test
中宣告全域變數 enable_rb_tree_sort
然後修改 do_sort
函式中的部份程式,用來判斷執行 sort
時要用什麼樣的方法排序
最後在 console_init
函式中加入對應的 option
的參數
執行時期可以任意變更排序方法
level order
time
命令測試 10000
筆資料的執行時間tree_sort
平均五次下來為 0.269 s
,
對比原本實作的 merge sort
平均五次的執行時間為 0.0086 s
有很大的差距。
想了一下會有這樣的結果第一個可能是我的實作的效率可能不好,還有改進的空間。
另一個可能是 Wikipedia 所提及的做一次排序就要重新建一次樹,也就是需要使用 malloc
配置額外的空間,相較之下原本實作的 merge sort
因為是 in-place,所以執行成本相對較低。
TODO: 引入 memory pool (課程測驗題已提過),預先配置好記憶體,之後重用該空間,從而縮減動態記憶體配置的成本。
參考 rv32emu 的 memory pool
後實作一個醜醜的簡易版本。
新增 mpool.h
和 mpool.c
在 mpool.h
定義一個結構體 mpool_t
,用來事先配置記憶體給紅黑樹,從而簡少 tree sort
配置記憶體的成本。
node_count
代表 memory pool
目前的節點個數。
mapped
代表配置的記憶體的起始位置。
接下來 mpool
提供的函式依序有:
mpool_create
用來建立 memory pool
,然後依照輸入的佇列的長度初始化。
mpool_alloc
在已經建立 memory pool
的情況下,判斷是否需要配置更多的記憶體,若是則 free
掉目前的空間後再重新配置一次,反之則什麼都不做返回。
重新配置記憶體的部份一開始是用 realloc
,不過目前會遇到以下的問題,還在 debug
中…。
可參考 rv32emu 的 mpool.c
中的 mpool_extend()
,避免 free 掉原先已配置好的記憶體
mpool_size
回傳目前 memory pool
的節點個數。
mpool_free
負責釋放 memory pool
動態配置出來的空間,然後把 node_count
歸零。
mpool_destory
釋放掉整個 mpool_t
結構體所用到的空間。
tree_sort
的修改tree_sort
的參數新增一個 node_t *node_t_pool
代表事先分配好的節點空間。
建立紅黑樹的部份改成以下程式,index
從零開始累加,node_t_pool + (index++)
表示該次分配的節點的位址。
最後在釋放記憶體的時候不需要釋放紅黑樹節點的空間。
負責建立以及初始化節點的函式 list_make_node
做一些小修改,將紅黑樹節點的位址通過參數傳入使用。
qtest
的修改整體的想法是要在 qtest
準備好 memory pool
,讓 treesort
使用。
所以新增了一個命令 mem_pool
負責做這件事。
首先是全域變數的宣告
mpool_t
結構體的指標 mp
以及 call_mem_pool_times
代表呼叫 mem_pool
的次數。
do_mem_pool
函式會先判斷輸入是否正確和佇列是否為空
然後依照 call_mem_pool_times
來判斷要呼叫什麼函式
如果是 call_mem_pool_times
等於零就建立及初始化 memory pool
,大於零則判斷目前空間是否夠用,不夠則重新配置。
最後會輸出目前的 memory pool
大小。
do_free
函式要多加入釋放 memory pool
的程式。
q_quit
函式同樣也要。
建立一個佇列 [1, 3, 0, 2]
然後用 mem_pool
命令建立對應大小紅黑樹節點空間
最後再用 sort
命令測試是否可以正常執行排序
如果對佇列插入 4
後再次使用 mem_pool
命令
則會擴增 memory pool
的空間
最後可以用 free
或直接 quit
都可以釋放掉 memory pool
的空間。
初步測試效能,以 10000
為基準:
平均五次下來為 0.0122 s
,與先前的實作的執行時間 0.269 s
相比大幅的下降。
不過與原實作的 merge sort
的 0.0086 s
相比還是較差。
TODO: 更詳細的效能比較或畫圖
memory pool
為了避免每次擴大 pool
都需要 free
,所以做進一步的修改,mpool
改為:
了解!
arena
儲存每次新增的節點記憶體的起始 (first_mapped
) 到最後 (last_mapped
) 的位址,next
表示下一個 arena
的位址。
最後的 mem_node_t
結構體為了避免在 qtest
會有重複宣告的情況發生,所以會先檢查是否有定義過。
mem_node_t
所儲存的資訊就是紅黑樹的節點以及下一個 mem_node_t
的位址。
mpool_create
也要做對應的修改,初始化 mpool_t *new_mp
的部份為:
配置與輸入佇列相同長度的 mem_node_t
空間後再將其連結起來,最後分別將其開頭與尾端位址存到 arena
的 first_mapped
和 last_mapped
中。
mpool_alloc
修改邏輯與 mpool_create
大致相同,只是需要先判斷目前的空間是否夠用。
如果不夠就宣告新的 arena
並且初始化。
最後把初始化後的 arena
接到 mpool_t
的最尾端 (last_arena
)。
mpool_free
負責釋放所有 arena
裡頭的 mem_node_t
空間以及 arena
結構體本身
然後把 arena_count
, node_count
歸零,first_arena
, last_arena
重新指向 NULL
。
queue.h
的修改
因為 treesort
函式需要用到 mem_node_t
結構體,所以再宣告一次 mem_node_t
。
修改 treesort
函式
透過 mem_node_t
的 next
指標來取得下一個節點的位址。
最後 qtest
的修改只需要將 tree_sort
函式改為對應的參數即可。
因應 qtest
原本的 do_sort
函式規定在做排序時不能動態配置記憶體的需求。
之前已經透過 memory pool
的方式事先配置節點的記憶體了,不過 tree sort
在建立 struct cmap_internal
時會用到 malloc
。
所以在 treesort
函式宣告區域變數來解決這個問題。
最後做個小測試:
mpool
為了能讓使用者可以自行定義 mpool
的結構體,參考 第 4 週測驗 1 的 rb.h 的寫法來改寫 mpool
。
原本的 mpool
的結構體是直接定義的,所以也就只能給 node_t
使用。
利用巨集改寫後的 mpool.h
,type
代表要儲存的結構體名稱,可以依照使用者的需求改變。
接著 mpool
的各個函式的實作由 mpool_gen
生成。
接下來各個函式的修改都是修改對應的結構體名稱而已
在 qtest
呼叫 mpool_proto
來產生各個函式的 prototype
,然後再用 mpool_gen
產生對應函式的實作。