LAB9 MALLOC - Xuehai-Chen/CSAPP GitHub Wiki
Writing a Dynamic Storage Allocator
说明
-
textbook中提供了一套简单的动态内存分配器实现,定义了链表格式,实现了大部分操作,但是find_fit和place没有提供。
-
对于动态内存分配器的优化,textbook中给出了几种思路,包括显示链表、适配分离、伙伴系统等。
简单隐式空闲链表
隐式链表格式与宏操作
-
链表包含一个头部和一个脚部,这两部分完全相同。之所以放置两部分,是因为在遍历链表或者获取前后链表项时,脚部的信息为这种操作提供了可能。
-
头部信息是8位对齐的,最后一位被设计为是否已被分配的标志位。
-
为了方便获取块大小、是否已被分配等头部信息,将这些通用的指针操作封装成了宏,避免出错。
#define WSIZE 4
#define DSIZE 8
#define CHUNKSIZE (1<<12)
#define MAX(x, y) ((x) > (y)? (x) : (y))
#define PACK(size, alloc) ((size) | (alloc))
#define GET(p) (*(unsigned int *)(p))
#define PUT(p, val) (*(unsigned int *)(p) = (val))
#define GET_SIZE(p) (GET(p) & ~0x7)
#define GET_ALLOC(p) (GET(p) & 0x1)
#define HDRP(bp) ((char *)(bp) - WSIZE)
#define FTRP(bp) ((char *)(bp) + GET_SIZE(HDRP(bp)) - DSIZE)
#define NEXT_BLKP(bp) ((char *)(bp) + GET_SIZE(((char *)(bp) - WSIZE)))
#define PREV_BLKP(bp) ((char *)(bp) - GET_SIZE(((char *)(bp) - DSIZE)))
find_fit
- textbook中没有给出find_fit的实现,这里采用首次适配。
static void *find_fit(size_t asize) {
void *current_head = heap_listp;
while (GET_SIZE(HDRP(current_head)) != 0) {
if (GET_SIZE(HDRP(current_head)) >= asize && !GET_ALLOC(HDRP(current_head))) return current_head;
current_head = NEXT_BLKP(current_head);
}
return NULL;
}
place
- place可以对内存块split,但是对小内存块的split很可能造成外部碎片。
static void place(void *bp, size_t asize) {
size_t size = GET_SIZE(HDRP(bp));
if (size >= (asize + 2 * DSIZE)) {
PUT(HDRP(bp), PACK(asize, 1));
PUT(FTRP(bp), PACK(asize, 1));
PUT(HDRP(NEXT_BLKP(bp)), PACK(size - asize, 0));
PUT(FTRP(NEXT_BLKP(bp)), PACK(size - asize, 0));
} else {
PUT(HDRP(bp), PACK(size, 1));
PUT(FTRP(bp), PACK(size, 1));
}
}
结果
Results for libc malloc:
trace valid util ops secs Kops
0 yes 0% 5694 0.000872 6531
1 yes 0% 5848 0.000697 8391
2 yes 0% 6648 0.001077 6172
3 yes 0% 5380 0.001238 4345
4 yes 0% 14400 0.000222 64953
5 yes 0% 4800 0.001516 3167
6 yes 0% 4800 0.001844 2603
7 yes 0% 12000 0.000708 16949
8 yes 0% 24000 0.000651 36872
9 yes 0% 14401 0.000359 40137
10 yes 0% 14401 0.000168 85874
Total 0% 112372 0.009351 12017
Results for mm malloc:
trace valid util ops secs Kops
0 yes 99% 5694 0.006692 851
1 yes 99% 5848 0.006121 955
2 yes 99% 6648 0.010305 645
3 yes 100% 5380 0.007419 725
4 yes 66% 14400 0.000134107704
5 yes 91% 4800 0.006518 736
6 yes 92% 4800 0.006229 771
7 yes 55% 12000 0.132141 91
8 yes 51% 24000 0.237033 101
9 yes 27% 14401 0.057591 250
10 yes 34% 14401 0.002065 6973
Total 74% 112372 0.472247 238
Perf index = 44 (util) + 16 (thru) = 60/100
分离适配
-
对简单隐式空闲链表的一个改进是,使用分离适配。
-
在分离适配策略中,分配器维护多个大小类空闲链表。处理一个分配请求时,首先对适当的大小类空闲链表做首次适配,如果未找到则在下一个大小类空闲链表中搜索。
-
采用分离适配后,主要的难点在于对于空闲链表的维护。
大小类定义及宏的扩展
-
空闲链表头部指针存储在一个大小为14的指针数组中。
-
大小类的定义采用textbook推荐的做法,即按照2的冥来组织不同大小的内存块。
-
分离适配中的空闲链表原则上可以被组织成某种类型的显式或隐式链表,这里采用单向显式链表的实现。在原payload部分的第1-4字节存放下一个链表项的指针。
-
一个巧妙的设计是,将内存块的头部信息的第二位作为是否有继承者的标志位。在一个内存块加入到空闲链表后,要检查原空闲链表是否为空。在移除时,要检查后续是否还有继承者。
#define GET_SUCC_BLKP(bp) ((void *)*(unsigned int *)(bp))
#define SET_SUCC_BLKP(bp, succ) (*(unsigned int *)(bp) = (unsigned int)(succ))
#define HAS_SUCC_BLKP(bp) (GET(bp) >> 1 & 0x1)
find_fit
- 这里的find_fit仍然是首次适配。
static void *find_fit(size_t asize) {
int index = ilog2(asize);
if (index > 13) index = 13;
while (index <= 13) {
void *current = free_list[index];
int count = 5;
while (current != NULL && GET_SIZE(HDRP(current)) != 0 && !GET_ALLOC(HDRP(current)) && count > 0) {
if (GET_SIZE(HDRP(current)) >= asize) return current;
current = HAS_SUCC_BLKP(HDRP(current)) ? GET_SUCC_BLKP(current) : NULL;
count--;
}
index++;
}
return NULL;
}
free_list_insert
-
free_list_insert用来将内存块加入到对应的空闲链表头部。
-
free_list_insert将在free结束时、place结束时被调用。place有可能分割内存块,此时要将剩余的内存块加入空闲链表。
static void free_list_insert(void *bp) {
size_t size = GET(HDRP(bp));
int index = ilog2(size);
if (index > 13) index = 13;
void *head = free_list[index];
free_list[index] = bp;
SET_SUCC_BLKP(bp, head);
if (head != NULL) PUT(HDRP(bp), GET(HDRP(bp)) | (0x2));
}
free_list_remove
-
free_list_remove用来将内存块从空闲链表中移除。
-
free_list_remove应该在coalesce、find_fit以后被调用。find_fit后该内存块即将被使用;coalesce时,该内存块合并后即将重新加入空闲链表,因此原来的内存块应当被移除。
-
这里要小心处理头部信息的设置。内存块的头部信息的第二位作为是否有继承者的标志位。在一个内存块加入到空闲链表后,要检查原空闲链表是否为空。在移除时,要检查后续是否还有继承者。
static void free_list_remove(void *bp) {
size_t size = GET(HDRP(bp));
int index = ilog2(size);
if (index > 13) index = 13;
void *current = free_list[index];
void *prev = current;
while (current != NULL && GET_SIZE(HDRP(current)) != 0 && !GET_ALLOC(HDRP(current))) {
void *succ = HAS_SUCC_BLKP(HDRP(current)) ? GET_SUCC_BLKP(current) : NULL;
if (current == bp) {
if (prev == current) {
free_list[index] = succ;
} else {
if (succ == NULL) PUT(HDRP(prev), GET(HDRP(prev)) & (~0x2));
else {
SET_SUCC_BLKP(prev, succ);
PUT(HDRP(prev), GET(HDRP(prev)) | 0x2);
}
}
break;
}
prev = current;
current = succ;
}
}
结果
Results for libc malloc:
trace valid util ops secs Kops
0 yes 0% 5694 0.000867 6570
1 yes 0% 5848 0.000686 8522
2 yes 0% 6648 0.001122 5923
3 yes 0% 5380 0.001043 5156
4 yes 0% 14400 0.000219 65723
5 yes 0% 4800 0.001596 3007
6 yes 0% 4800 0.001669 2876
7 yes 0% 12000 0.000795 15085
8 yes 0% 24000 0.001002 23954
9 yes 0% 14401 0.000441 32648
10 yes 0% 14401 0.000190 75636
Total 0% 112372 0.009632 11667
Results for mm malloc:
trace valid util ops secs Kops
0 yes 98% 5694 0.000228 24963
1 yes 94% 5848 0.000222 26366
2 yes 98% 6648 0.000290 22932
3 yes 99% 5380 0.000232 23210
4 yes 66% 14400 0.000542 26563
5 yes 80% 4800 0.001823 2634
6 yes 77% 4800 0.001359 3532
7 yes 55% 12000 0.008105 1481
8 yes 51% 24000 0.029201 822
9 yes 30% 14401 0.059474 242
10 yes 45% 14401 0.002583 5576
Total 72% 112372 0.104058 1080
Perf index = 43 (util) + 40 (thru) = 83/100
-
可以看到对比于简单隐式链表的实现有了比较大的提升,但还算不上让人满意。
-
关于结果中的吞吐量,mm实现大约是libc的十分之一,特别是随着操作数变大,kops显著减小。原因也是显而易见的,因为mm实现采用了首次适配,在遍历空闲链表时的性能较差,从而导致吞吐量随着操作数变大而减小。
-
关于结果中的空间利用率,mm实现大约是libc的1.39倍,甚至比简单隐式链表的实现还要差。一方面可能是大小类的数量太小,导致内部碎片过多;另一方面因为采用了首次适配,因此外部碎片也会比较多。