实验简介
实现自己的动态内存分配器(malloc、free、realloc)。
预备知识
- 阅读《CSAPP原书第3版》 9.9小节 —— 动态内存分配。
- 阅读writeup的全部内容。
分配器的设计要求
- 处理任意请求序列,分配器不可以假设分配和释放请求的顺序。
- 立即响应请求, 不允许分配器为了提高性能重新排列或缓冲请求。
- 只使用堆。
- 对齐块,以保存任何类型的数据对象。
- 不修改已分配的块,分配器只能操作和改变空闲块。
分配器的设计目标
- 最大化吞吐率 —— 每个
malloc, free执行的指令越少,吞吐率会越好。
- 最大化内存利用率。
实现问题
关键是把握吞吐率和内存利用率之间的平衡。
- 空闲块组织 —— 如何记录空闲块?
- 放置 —— 如何选一个合适的空闲块来放置一个新分配的块? (首次适配/下次适配/最优适配)
- 分割 —— 将一个新分配块放到某个空闲块后,如何处理这个空闲块的剩余部分?
- 合并 —— 如何处理一个刚被释放的块? (立即合并/延迟合并)
实验步骤
代码下载:http://csapp.cs.cmu.edu/3e/malloclab-handout.tar
目标是实现mm.c中的如下函数, 原型如下:
1 2 3 4
| int mm_init(void); void *mm_malloc(size_t size); void mm_free(void *ptr); void *mm_realloc(void *ptr, size_t size);
|
这里使用两种方式实现malloc,分别如下:
- 隐式空闲链表 + 首次适配/下一次适配。
- 显示空闲链表 + 分离的空闲链表 + 分离适配。
隐式空闲链表法
原书9.9.6节详细介绍了隐式空闲链表法,并贴出了所有源代码。代码实现细节请参考原书或者 https://github.com/PCJ600/MallocLab/tree/br64
隐式空闲链表的形式如下:
![]()
- 每个堆块使用边界标记法。头部大小为4字节,前29位表示块大小,后3位表示这个块是否空闲;脚部(ftr)是头部(hdr)的副本。目的是将合并前面的堆块时的搜索时间降到常数。
- 第1个填充字用于8字节对齐访问。考虑64位场景,如不添加填充字,heap_listp的值不能整除8,不满足对齐条件!
- 序言块和结尾块的设计是消除合并时边界条件的技巧。
- 按8字节对齐要求, 一个堆块最小为 4(头部) + 8(payload) + 4(脚部) = 16字节
- 为什么是”隐式”的?—— 因为空闲块是通过头部中大小字段隐含地连接着,从而间接遍历整个空闲块的集合。
1. 初始化堆 —— mm_init函数
mm_init步骤如下:
2. 扩展堆 —— extend_heap函数
函数原型: static void *extend_heap(size_t words);
以下两种场景需要扩展堆:
- 调用
mm_init初始化堆时。
- 调用
mm_malloc找不到合适的空闲块时。
举例:堆上扩展4096个字节,堆数组前后变化如下:
![]()
3. 释放和合并块 —— mm_free和coalesce函数
调用mm_free释放块,步骤如下:
调用coalesce合并前后的合并块,原型:static void *coalesce(void *bp);,分四种情况:
- 情况1:前面的块和后面的块都已分配 —— 不可能合并,简单返回bp即可。
- 情况2:前面的块已分配,后面的块空闲 —— 用当前块和后面块的大小之和更新当前块的头部和后面块的脚部。返回bp
- 情况3:前面的块是空闲的,后面的块是分配的 —— 用两块大小之和更新前面块的头部和后面块的脚部。返回
PREV_BLKP(bp)
- 情况4:前面和后面的块都是空闲的 —— 用三个块大小之和更新前面块的头部和后面块的脚部。返回
PREV_BLKP(bp)
说的比较啰嗦,以下画图帮助理解:
情况2: 前面的块已分配,后面的块空闲
![]()
注意: 如采用下次适配策略,在情况3、情况4合并后可能出现pre_listp指针不再指向一个块的payload段,报payload overlap错!
因此必须更新pre_listp。这里简单将pre_listp指向合并后的新块的payload即可。
情况3: 前面的块是空闲的,后面的块是分配的
![]()
情况4: 前面和后面的块均空闲
![]()
[O#### 4. 分配块 —— mm_malloc
mm_malloc步骤
适配算法
分配器搜索空闲块的方式由放置策略决定,常见策略有首次适配、下一次适配等。
分割策略
如分割后剩下的块不小于最小块大小(16字节),才分割这个块。
设空闲块大小为M字节,malloc请求的块大小为N字节。只有M - N >= 16,才分割这个块。
![]()
5. 实现mm_realloc
mm_realloc原型:void *mm_realloc(void *ptr, size_t size)
writeup中提到了mm_realloc的所有实现要点,如下:
- 如果ptr为NULL, 等价于调用mm_malloc
- 如果size为0, 等价于调用mm_free
- 如ptr不为NULL且size不为0, 参考realloc函数的实现:
man 3 realloc
实验结果
执行./mdriver -t traces/ -V,查看详细结果:
首次适配: 44 (util) + 24 (thru) = 68/100
下一次适配:43 (util) + 40 (thru) = 83/100
分离空闲链表法
实现代码参考:https://github.com/PCJ600/MallocLab
使用分离的空闲链表,分配器会维护一个空闲链表的数组。每个空闲链表和一个大小类关联,被组织成某种类型的显式或隐式链表。笔者这里使用以下方案:
- 链表结构为显式的双向链表
- 大小类分为 {16-31},{32,63},{64,127}, …, {4096, 8191}, … 链表个数
MAX_LIST_NUM 默认设置为20,可调整。
- 考虑兼容性,分配器需要在32位/64位环境下都能正常运行。
注意事项
- 32位机器上,指针大小为4字节;64位机器上, 指针大小为8字节。可使用
sizeof(intptr_t)表示指针大小, intptr_t类型是ISO C99定义的,可参考/usr/include/stdint.h
- 实验要求不使用全局变量,可以将分离链表的头指针放到堆中。
- 默认Makefile采用
-m32选项,64位环境下需要改成-m64。
- 实验涉及大量指针操作,编码极易出错。需掌握基本的gdb调试手段、并编写代码检查堆区和分离链表。
显式的双向链表的堆块结构
![]()
对于空闲块,pred保存上一个空闲块的地址,succ保存下一个空闲块的地址。
使用双向链表结构,适配算法的时间复杂度从O(块总数)降到O(空闲块总数)。
不难得出:32位系统,块至少为16字节;64位系统,块至少为24字节。
如何调试?
- 设置编译选项
-g -O0取消编译优化。
- 设置编译选项
-g3 -gdwarf-2调试宏。
- 可以设置
-DDEBUG宏,通过编译宏控制是否打印调试信息。
- 实现
mm_print函数,在gdb中通过call mm_print()打印堆区和分离链表。
打印堆数组状态和所有分离链表 —— mm_print函数设计
堆区和分离链表检查 —— mm_check函数设计
检查堆区状态,包括:
- 检查序言块、结尾块的指针、大小、分配位是否正确。
- 检查每个块的payload指针是否满足对齐要求。
- 检查每个块的payload指针是否在堆区的合法地址范围内(
mem_heap_lo() ~ mem_heap_hi()之间)。
- 检查每个块的头部和脚部是否一致。
- 检查每个块的大小是否不低于最小块的大小,是否为4/8字节的倍数。
- 采用立即合并策略时,检查不存在任意两个相邻的空闲块。
检查分离链表状态,包括:
检查链表中所有指针是否在堆区的合法地址范围内。
检查双向链表实现是否正确,是否每个指针A的后继为B时,B的前驱也同时为A。
检查分离链表中所有的空闲块是否与堆数组的空闲块中找到并匹配。
检查堆数组中每个空闲块是否都能在分离链表中找到并匹配。
检查堆数组中每个已占用块是否都不在分离链表中。
针对malloc做如下检查:
- malloc返回前,检查指针p是否在堆数组中,如不在堆数组中说明出错。
- malloc返回前,检查指针p对应的块大小是否不小于malloc请求的大小。
针对free做如下检查:
实现代码参考: https://github.com/PCJ600/MallocLab/blob/master/mm.c mm_check函数
指针运算、宏定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| #define ALIGNMENT (sizeof(size_t)) #define ALIGN(size) (((size) + (ALIGNMENT-1)) & ~(ALIGNMENT-1))
#define WSIZE 4 #define DSIZE 8 #define CHUNKSIZE (1 << 12) #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) & ~(0x1)) #define GET_ALLOC(p) (GET(p) & 0x1)
#define GET_P(p) (*(intptr_t *)(p)) #define PUT_P(p, val) (*(intptr_t *)(p) = (intptr_t)(val))
#define MAX_LIST_NUM 20 #define MIN_INDEX 4
#define MIN_BLOCK_SIZE (DSIZE + 2 * sizeof(intptr_t)) #define PTR(bp) ((char *)(bp))
#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)))
#define PREV(bp) ((char *)(bp)) #define SUCC(bp) ((char *)(bp) + sizeof(intptr_t))
#define GET_PREV(bp) ((char *)(GET_P(PREV(bp)))) #define GET_SUCC(bp) ((char *)(GET_P(SUCC(bp))))
|
辅助函数设计
1 2 3 4 5 6 7 8 9 10 11
| static void insert_node(void *p, size_t size); static void delete_node(void *p); static void *coalesce(void *p); static void *place(void *p, size_t size); static void *extend_heap(size_t size);
static void *find_free_block(size_t size);
static void insert_node_by_list_index(void *p, size_t size, int idx);
static int delete_node_by_list_index(void *p, int size, int idx);
|
insert_node
1 2 3 4 5 6 7 8 9 10 11
| static void insert_node(void *p, size_t size) { int list_size; for (int i = 0; i < MAX_LIST_NUM; ++i) { list_size = (1 << (MIN_INDEX + i)); if (size > list_size) { continue; } insert_node_by_list_index(p, size, i); return; } }
|
delete_node
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| static void delete_node(void *p) { int i; int list_size; int size = GET_SIZE(HDRP(p)); for (i = 0; i < MAX_LIST_NUM; ++i) { list_size = (1 << (MIN_INDEX + i)); if (size <= list_size) { break; } } for ( ; i < MAX_LIST_NUM; ++i) { if (delete_node_by_list_index(p, size, i)) { return; } } }
|
coalesace
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| static void *coalesce(void *p) { int prev_alloc = GET_ALLOC(HDRP(PREV_BLKP(p))); int next_alloc = GET_ALLOC(HDRP(NEXT_BLKP(p))); int size = GET_SIZE(HDRP(p));
if (prev_alloc && next_alloc) { return p; }
if (prev_alloc && !next_alloc) { delete_node(p); delete_node(NEXT_BLKP(p)); size += GET_SIZE(HDRP(NEXT_BLKP(p))); PUT(HDRP(p), PACK(size, 0)); PUT(FTRP(p), PACK(size, 0)); } else if (!prev_alloc && next_alloc) { delete_node(PREV_BLKP(p)); delete_node(p); size += GET_SIZE(HDRP(PREV_BLKP(p))); PUT(FTRP(p), PACK(size, 0)); PUT(HDRP(PREV_BLKP(p)), PACK(size, 0)); p = PREV_BLKP(p); } else { delete_node(PREV_BLKP(p)); delete_node(p); delete_node(NEXT_BLKP(p)); size += GET_SIZE(HDRP(PREV_BLKP(p))) + GET_SIZE(HDRP(NEXT_BLKP(p))); PUT(FTRP(NEXT_BLKP(p)), PACK(size, 0)); PUT(HDRP(PREV_BLKP(p)), PACK(size, 0)); p = PREV_BLKP(p); } insert_node(p, size); return p; }
|
place
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
static void *place(void *p, size_t size) { int max_size = GET_SIZE(HDRP(p)); int delta_size = max_size - size; delete_node(p); if (delta_size < MIN_BLOCK_SIZE) { PUT(HDRP(p), PACK(max_size, 1)); PUT(FTRP(p), PACK(max_size, 1)); return p; }
PUT(HDRP(p), PACK(size, 1)); PUT(FTRP(p), PACK(size, 1)); PUT(HDRP(NEXT_BLKP(p)), PACK(delta_size, 0)); PUT(FTRP(NEXT_BLKP(p)), PACK(delta_size, 0)); insert_node(NEXT_BLKP(p), delta_size); return p; }
|
extend_heap
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| static void *extend_heap(size_t size) { size = ALIGN(size); void *p; if ((p = mem_sbrk(size)) == (void *)-1) { printf("extend_heap failed! mem_sbrk return -1!\n"); return NULL; }
PUT(HDRP(p), PACK(size, 0)); PUT(FTRP(p), PACK(size, 0)); PUT(HDRP(NEXT_BLKP(p)), PACK(0, 1)); insert_node(p, size); return coalesce(p); }
|
初始化堆 —— mm_init
调用mm_init后,堆数组结构如下图所示:
![]()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| int mm_init(void) { char *p = mem_sbrk(MAX_LIST_NUM * sizeof(intptr_t) + 4 * WSIZE); if ((void *)p == (void *)(-1)) { return -1; }
for (int i = 0; i < MAX_LIST_NUM; ++i) { PUT_P(p + i * sizeof(intptr_t), NULL); } p += MAX_LIST_NUM * sizeof(intptr_t);
PUT(p, 0); PUT(p + WSIZE, PACK(DSIZE, 1)); PUT(p + 2 * WSIZE, PACK(DSIZE, 1)); PUT(p + 3 * WSIZE, PACK(0, 1));
if ((p = extend_heap(CHUNKSIZE)) == NULL) { return -1; } return 0; }
|
分配块 —— mm_malloc
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| void *mm_malloc(size_t size) { size = get_malloc_size(size); void *p = find_free_block(size); if (p == NULL) { if ((p = extend_heap(MAX(size, CHUNKSIZE))) == NULL) { printf("mm_malloc, extend_heap failed!\n"); return NULL; } } p = place(p, size); return p; }
|
释放块 —— mm_free
1 2 3 4 5 6 7 8 9
| void mm_free(void *ptr) { int size = GET_SIZE(HDRP(ptr)); PUT(HDRP(ptr), PACK(size, 0)); PUT(FTRP(ptr), PACK(size, 0)); insert_node(ptr, size); coalesce(ptr); }
|
重分配块 —— mm_realloc
函数原型:void *mm_realloc(void *p, size_t size), 优化点如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| void *mm_realloc(void *ptr, size_t size) { if (ptr == NULL || size == 0) { return NULL; } size = get_malloc_size(size); int old_size = GET_SIZE(HDRP(ptr)); if (old_size >= size) { return ptr; }
int next_alloc = GET_ALLOC(HDRP(NEXT_BLKP(ptr))); int next_size = GET_SIZE(HDRP(NEXT_BLKP(ptr))); if (!next_alloc && (next_size >= size - old_size)) { delete_node(NEXT_BLKP(ptr)); PUT(HDRP(ptr), PACK(next_size + old_size, 1)); PUT(FTRP(ptr), PACK(next_size + old_size, 1)); return ptr; }
void *oldptr = ptr; ptr = mm_malloc(size); memcpy(ptr, oldptr, old_size); mm_free(oldptr); return ptr; }
|
实验结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| # ./mdriver -t traces/ -V Results for mm malloc: trace valid util ops secs Kops 0 yes 99% 5694 0.000886 6430 1 yes 99% 5848 0.000793 7379 2 yes 99% 6648 0.000903 7359 3 yes 99% 5380 0.000749 7182 4 yes 66% 14400 0.001754 8212 5 yes 96% 4800 0.001114 4308 6 yes 95% 4800 0.001112 4317 7 yes 55% 12000 0.004104 2924 8 yes 51% 24000 0.014884 1612 9 yes 87% 14401 0.001490 9667 10 yes 67% 14401 0.001074 13405 Total 83% 112372 0.028862 3893
Perf index = 50 (util) + 40 (thru) = 90/100
|
参考资料
《深入理解计算机系统 原书第3版》
https://littlecsd.net/2019/02/14/csapp-Malloclab/
https://www.cnblogs.com/liqiuhao/p/8252373.html