contributed by < weiso131
>
1
AAAA
: &l
BBBB
: before
CCCC
: &(*p)->next
DDDD
: item->next
list_item_t *merge(list_item_t *l, list_item_t *r, int descend)
{
list_item_t *head = NULL, **indir = &head;
while (1) {
if (l->value > r->value == descend) {
*indir = l;
indir = &l->next;
l = l->next;
if (!l) {
*indir = r;
break;
}
}
else {
*indir = r;
indir = &r->next;
r = r->next;
if (!r) {
*indir = l;
break;
}
}
}
return head;
}
list_item_t *merge_sort(list_item_t *head, int descend)
{
if (head->next == NULL)
return head;
list_item_t *l = head, *last_r = head, *r = head->next, *fast = head->next;
while (fast != NULL && fast->next != NULL) {
last_r = r;
r = r->next;
fast = fast->next->next;
}
last_r->next = NULL;
l = merge_sort(l, descend), r = merge_sort(r, descend);
return merge(l, r, descend);
}
2
EEEE
: (*pred_ptr)->r
FFFF
: &(*pred_ptr)->r
remove_free_tree
解讀l == r == NULL
l != NULL || r != NULL
l != NULL && r != NULL
*pred_ptr != (*node_ptr)->l
*pred_ptr == (*node_ptr)->l
block_t **find_free_tree(block_t **root, block_t *target)
{
block_t **node_ptr = root;
while (*node_ptr != target && *node_ptr != NULL) {
if (target->size > (*node_ptr)->size)
node_ptr = &(*node_ptr)->r;
else
node_ptr = &(*node_ptr)->l;
}
return node_ptr;
}
void insert_free_tree(block_t **root, block_t *target)
{
block_t **node_ptr = find_free_tree(root, target);
*node_ptr = target;
}
block_t *init_block(size_t size)
{
block_t *block = malloc(sizeof(block_t));
block->size = size;
block->l = block->r = NULL;
return block;
}
void block_test()
{
for (int i = 0;i < N;i++) {
size_t size = rand() % 8192;
blocks[i] = init_block(size);
insert_free_tree(&root, blocks[i]);
}
for (int i = 0;i < N;i++) {
remove_free_tree(&root, blocks[i]);
block_t **find = find_free_tree(&root, blocks[i]);
assert(*find == NULL);
free(blocks[i]);
blocks[i] = NULL;
}
printf("All test complete!!!\n");
}
github
參考 memory-allocators 的 free list allocator。
這種記憶體分配器屬於通用型分配器,利用一個資料結構(通常是鏈結串列或紅黑樹)紀錄可供使用的記憶體區塊,在使用者請求分配時,在這個資料結構尋找合適的區塊提供給使用者。
另外,若在釋放記憶體時,發現有相鄰的記憶體區塊是可用的,這種實作方法會將其合併。
要以時間複雜度
文中提到可以用鍊結串列或是紅黑樹維護,我嘗試使用二元搜尋樹代替紅黑樹,實作這種記憶體分配器。在不發生樹嚴重傾斜的情況下,查詢的時間複雜度與紅黑樹相同,皆是
至於雙向鏈結串列,在實作過程中發現,若是環狀的鏈結串列,在插入節點時就沒有判斷下一項是否為空指標的問題,因此我使用 list.h 的資料結構與 API 來維護這個雙向鏈結串列。
這些是這個分配器提供給使用者的主要功能
定義在 includes/free_list_alloc.h
void init_fl(size_t size);
void *fl_alloc(size_t size)
void fl_free(void *ptr)
init_fl
fl_alloc
find_block_by_size
fl_free
try_merge
merge_b_to_a
使用 perf 尋找效能瓶頸
(*node_ptr)->size
0.07 │25: mov -0x20(%rbp),%rax ▒
0.71 │ mov (%rax),%rax ▒
0.13 │ mov -0x8(%rbp),%rdx ▒
53.47 │ mov (%rdx),%rdx
find_free_tree
使用效能或許有些微提昇,使用 t 檢定驗證
def calculate_cohens_d(x1, x2):
# Pooled standard deviation
nx = len(x1)
ny = len(x2)
pooled_std = np.sqrt(((nx - 1)*np.var(x1, ddof=1) + (ny - 1)*np.var(x2, ddof=1)) / (nx + ny - 2))
d = (np.mean(x1) - np.mean(x2)) / pooled_std
return d
def compare_groups(free_list, new_free_list):
# Welch's t-test(自動考慮變異數不等)
t_value, p_value = ttest_ind(free_list, new_free_list, equal_var=False)
# 自由度由 ttest_ind 自動處理(Welch 方法)
cohen_d = calculate_cohens_d(free_list, new_free_list)
print(f"✅ Welch's t-test 結果:")
print(f"t 值:{t_value:.4f}")
print(f"p 值:{p_value:.4f} {'(顯著)' if p_value < 0.05 else '(不顯著)'}")
print(f"Cohen's d 效果量:{cohen_d:.4f}")
print(f"效能提升倍率:{free_list.sum() / new_free_list.sum():.4f}")
compare_groups(free_list, new_free_list)
輸出:
✅ Welch's t-test 結果:
t 值:54.8427
p 值:0.0000 (顯著)
Cohen's d 效果量:0.0776
效能提升倍率:1.0997
3
GGGG
: head->prev=prev
HHHH
: list_entry(pivot,node_t,list)->value
IIII
: list_entry(n,node_t,list)->value
JJJJ
: pivot
KKKK
: right
rebuild_list_link
解說node 不斷往 next
prev 緊跟在 node 後面
當 node 為 NULL 時,代表鍊結串列的最後一個節點找到了
此時 prev 就是最後一個節點
quick_sort
解說稍微跳過幾個步驟
AAAA
: list_first_entry
BBBB
: list_del
CCCC
: list_move_tail
DDDD
: list_add
EEEE
: list_splice
FFFF
: list_splice_tail
list_quicksort
解釋random_shuffle_array
使用 4 個元素的陣列
執行 shuffle 1e6 次
紀錄每種排列的出現次數
再計算卡方值
static int index_form[4][4][4];
#define SHUFFLE_TIMES 1000000
#define FACTORIAL_4 24
void build_lookup_table()
{
int index = 0;
uint16_t perm[4] = {1, 2, 3, 4};
// Generate all 4! permutations using stdlib's next_permutation
for (int a = 1; a <= 4; ++a)
for (int b = 1; b <= 4; ++b)
if (b != a)
for (int c = 1; c <= 4; ++c)
if (c != a && c != b)
for (int d = 1; d <= 4; ++d)
if (d != a && d != b && d != c)
index_form[a - 1][b - 1][c - 1] = index++;
}
int main()
{
uint16_t array[4] = {0, 1, 2, 3};
uint32_t count[FACTORIAL_4] = {0};
build_lookup_table();
for (int i = 0; i < SHUFFLE_TIMES; ++i) {
random_shuffle_array(array, 4);
int idx = index_form[(int)array[0]][(int)array[1]][(int)array[2]];
count[idx]++;
}
double expected = (double)SHUFFLE_TIMES / FACTORIAL_4;
double chi_squared = 0.0;
for (int i = 0; i < FACTORIAL_4; ++i) {
double diff = count[i] - expected;
chi_squared += (diff * diff) / expected;
}
printf("Chi-squared value: %.4f\n", chi_squared);
return 0;
}
random_shuffle_array
輸出:
Chi-squared value: 122688.7850
卡方值遠高於
fisher_yates_shuffle
參考 lab0 提供的 shuffle 演算法
static inline void fisher_yates_shuffle(uint16_t *operations, uint16_t len)
{
for (uint16_t i = 0;i < len;i++) {
uint16_t new = len - i - 1;
uint16_t old = get_unsigned16() % (new + 1);
uint16_t save = operations[new];
operations[new] = operations[old];
operations[old] = save;
}
}
輸出:
Chi-squared value: 21748.5402
雖然有減少,但其卡方值仍遠高於
rand
代替 get_unsigned16
uint16_t new = len - i - 1;
- uint16_t old = get_unsigned16() % (new + 1);
+ uint16_t old = rand() % (new + 1);
uint16_t save = operations[new];
輸出:
Chi-squared value: 10.5795
卡方值小於
無證據說明這個 shuffle 是不公平的
list_move_tail
換為 list_move
無法滿足 stable sortingGGGG
: 14
HHHH
: 2
IIII
: 0
JJJJ
: 3
KKKK
: 2
LLLL
: 1
MMMM
: ~1
NNNN
: 1
PPPP
: 2
clz32
upper
與 lower
upper
是否為 0 , 若不是 0 ,代表 leading zero 在 upper
裡面,對 upper
執行 clz32。反之, leading zero 在 lower , 此時加上 (16 >> c)
正是前方的位元數量。c == 3
, 此時 upper
與 lower
都只剩兩個 bits 的長度,若 upper
不為 0 , 將 upper
帶入 magic
可做最後的計算。反之, +2
然後對 lower
最類似的事情。最後只剩 2 bits , 代表會有四種可能
sqrti
題目中的 sqrti
其實就是 Digit-by-digit Method 的實作,但直接從原本的函式實作解釋它有些困難,我將會提供從最直觀的實作,一步一步優化到現在狀態的過程。
int shift = (total_bits - 1 - clz64(x)) & ~1;
while (shift >= 0) {
m = 1ULL << shift;
uint64_t m_2 = 1ULL << (shift >> 1);
uint64_t b = ((y * m_2) << 1) + m;
if (x >= b) {
x -= b;
y += m_2;
}
shift -= 2;
}
這個實作方法基本上是照著演算法的描述一步一步實現,可以看到他會存在一個乘法運算
int shift = (total_bits - 1 - clz64(x)) & ~1;
uint64_t last = 0;
while (shift >= 0) {
m = 1ULL << shift;
uint64_t m_2 = 1ULL << (shift >> 1);
uint64_t b = last + m;
last >>= 1;
if (x >= b) {
x -= b;
y += m_2;
last += m;
}
shift -= 2;
}
這個方法由一個 last
儲存每一次 y
更新時的 m
並在進入判斷式前右移一格
這個調整仍是有效的原因,是因為所有 m
都是 2 的次方
以此改寫公式
再展開
觀察到
這正是 last
在做的事情
last
與 y
int shift = (total_bits - 1 - clz64(x)) & ~1;
while (shift >= 0) {
m = 1ULL << shift;
uint64_t b = y + m;
y >>= 1;
if (x >= b) {
x -= b;
y += m;
}
shift -= 2;
}
在 shift
為 m
:
由於到迴圈結束時會被右移 last
而言相當於每次都加上
這與 y
的功能完全相同
到了這步基本上已經跟原本的 sqrti
相同,只差把每次重新計算的 m
放在迴圈外, 每次 m
往右移兩格的調整
x
在計算過程會被減去 y
的平方
若最後 x
不是 0 ,就代表 x
原本不是完全平方數,需要向上取整,也就是額外 +1
因此,可以對最後的 return
做修改
return y + (x && 1);
即可在只使用加減法與位元操作的情況下,完成向上取整的平方根
AAAA
: map->bits
BBBB
: map->bits
CCCC
: first->pprev
DDDD
: n->pprev
EEEE
: n->pprev