Lab7 Malloc Lab
写在前言:这个实验的来源是CSAPP官网:CSAPP Labs ,如果感兴趣的话,可以点击这个链接🔗去下载。实验中的10个traces文件是没有附加的,可以点击这个🔗:traces file 自行下载。
实验说明
Malloc Lab实验要求我们实现一个动态内存分配器(Dynamic Memory Allocator),要求我们实现与标准C库中的malloc
、free
、realloc
具有相同功能的函数,可以自行定义块的空间结构。
唯一需要修改的文件是mm.c
,里面声明了以下四个函数:
int mm_init(void);
void *mm_malloc(size_t size);
void mm_free(void *ptr);
void *mm_realloc(void *ptr, size_t size);
mm_init
:在调用其它三个函数之前,mdriver
会先调用mm_init
进行必要的初始化,比如分配初始堆区,如果初始化不成功,返回-1,否则返回0。mm_malloc
:返回已分配块的有效载荷payload
的起始地址,其中,payload
的大小至少为size
字节,除此之外,必须保证已分配块在堆区里面,并且不会与其它已分配块重叠。mm_free
:释放被ptr
指向的已分配块(调用mm_malloc
、mm_realloc
得到的ptr
),如果已经被释放,Nothing to do。mm_realloc
:返回一个已分配的块,其有效载荷payload
的大小至少为size
字节,并且有如下的限制:- 如果
ptr
为NULL
,那么等价于调用mm_alloc(size)
; - 如果
size
等于0,那么的等价于调用mm_free(ptr)
; - 否则,
mm_realloc
必须重新分配payload
至少为size
字节的块,并且保证新的块的内容和旧的块一致,值得注意的是,这个新块的地址有可能和旧块的地址相同,或者不同,取决于我们的实现方式。
- 如果
性能评价指标
对于给定的n个分配和释放请求的某种顺序
R 0 , R 1 , ⋯ , R k ⋯ , R n − 1 R_0,R_1,\cdots,R_k\cdots,R_{n-1} R0,R1,⋯,Rk⋯,Rn−1
如果一个应用程序请求一个p字节的块,那么得到的已分配块的有效载荷(payload)就是p字节,在请求 R k R_k Rk完成后,聚集有效载荷(aggregate payload)表示为 P k P_k Pk,为当前已分配块的有效载荷之和, H k H_k Hk表示为堆当前的大小(单调非递减)。
那么前k+1请求的 峰值利用率,表示为 U k = max i ≤ k P i H k U_k=\frac{\max_{i\le k}P_i}{H_k} Uk=Hkmaxi≤kPi,越接近1表示空间利用率越高。
其次,吞吐率,表示单位时间内完成的请求个数,比如,如果分配器1秒内完成了500个分配请求和500个释放请求,那么它的吞吐率就是每秒1000次操作。
总的来说,分配器的目标就是在整个序列中使得峰值利用率 U n − 1 U_{n-1} Un−1最大化,并且峰值利用率和吞吐率是互相牵制的,找到一个合适的折中就显得很重要。
mdriver
会记录空间利用率和吞吐率,与标准C库的吞吐率进行比较,最终评价指标为:
P = w U + ( 1 − w ) min ( 1 , T T l i b c ) P=wU+(1-w)\min(1,\frac{T}{T_{libc}}) P=wU+(1−w)min(1,TlibcT)
其中, w = 0.6 w=0.6 w=0.6,这意味着空间利用率的占比更高。
相关知识
下图所示为用户内存空间的结构,动态内存分配器管理的是堆区。
碎片现象
为了提高分配器的性能,首先就要提高内存的空间利用率,需要理解造成空间利用率低的原因:碎片现象(Fragmentation),分为内部碎片和外部碎片。
- 内部碎片,就是在一个已分配的块比有效载荷大的时候发生的,与块的空间结构,如是否有头部块和尾部块,或者与填充一些字节以满足对齐要求等有关。
- 外部碎片,当空闲内存合计足够一个分配请求,但是没有一个单独的空闲块足够大满足这个请求发生的。
内存分配器的实现
- 空闲块组织:如何记录空闲块?
- 放置:如何选择一个合适的空闲块放置一个新分配的块(首次适配、下一次适配、最佳适配)
- 分割:在一个新分配快放置到某一个空闲块之后,如何处理空闲块的剩余部分?
- 合并:如何处理一个刚被释放的块。
空闲块组织
任何实际的分配器都需要一些数据结构,允许它来区别块的边界一级区别已分配块和空闲块。
一个简单的堆块形式如下:
上述块的结构包括一个字的头部、有效载荷以及用于对齐而进行的填充组成的。
这种块结构间接地形成了一种链表结构,叫隐式空闲链表,它的结构是比较简单的。
放置已分配的块
当一个应用请求一个k字节的块,分配器搜索空闲链表,查找一个足够大的可以放置所请求块的空闲块,分配器执行这种搜索的方式由 放置策略 确定,常见的策略:首次适配,下一次适配,最佳适配
- 首次适配,从头开始搜索空闲链表,选择第一个合适的空闲块
- 下一次适配,从上一次查询结束的地方开始,选择第一个合适的空闲块
- 最佳适配,检查每一个空闲块,选择所需请求大小的最小空闲块
实际上,下一次适配比首次适配运行明显要快一些,但是内存利用率比首次适配利用率低很多,在简单的空闲链表组织结构中,比如隐式空闲链表中,使用最佳适配的缺点是它要求对堆进行彻底的搜索。
带边界标记的合并
Knuth提出了一种技术叫边界标记,允许在常数时间内继续块合并操作。
合并的情况分为四种:
- 前面的块和后面都是已分配的,Nothing to do。
- 前面的块是已分配的,后面的块是空闲的。
- 前面的块是空闲的,而后面的块是已分配的。
- 前面的和后面的块都是空闲的。
边界标记的概念很简单,对于不同类型的分配器和空闲链表的组织都是通用的,然而,它也存在一个潜在的缺陷,要求每个块都保持一个头部和脚部,如果应用请求很多小块时,会产生显著的内存开销。
对这个方法进行优化,可以使得已分配的块中不再需要脚部,只有在前面的块是空闲的时候才需要有脚部,如果把前面块的已分配/空闲位存放在当前块中多出来的低位中,那么已分配块就不需要脚部了,这样就可以讲个多出来的空间用作有效载荷了,需要注意的是,空闲块仍然需要脚部。
使用隐式空闲链表
第一种实现方式就是书本上给出的简单分配器实现,使用的块结构为未优化的带边界标记的堆块形式。
但是CSAPP书本上没有给出realloc
的实现方式,这需要我们自行实现。
这里的隐式空闲链表的结构包括序言快、若干个普通块(已分配/空闲)、以及一个结尾块,值得注意的是,添加序言块和结尾块实现是比较方便的,比如,在合并操作时,不需要进行边界的处理,结尾块是一个大小为0的已分配块,它标记着堆的结束位置。
首先是,操作链表的基本常数和宏定义:
/* Base constants and macros */
#define WSIZE 4 /* Word and header/footer size (bytes) */
#define DSIZE 8 /* Double word size (bytes) */
#define CHUNKSIZE (1<<12) /* Extend heap by this amount (bytes) */
#define MAX(x, y) ((x) > (y) ? (x) : (y))
/* Pack a size and allocated bit into a word*/
#define PACK(size, alloc) ((size) | (alloc))
/* Read and write a word at address p */
#define GET(p) (*(unsigned int *)(p))
#define PUT(p, val) (*(unsigned int *)(p) = val)
/* Read the size and allocated fields from address p */
#define GET_SIZE(p) (GET(p) & ~0x7)
#define GET_ALLOC(p) (GET(p) & 0x1)
/* Given block ptr bp, compute address of its header and footer */
#define HDRP(bp) ((char* )(bp) - WSIZE)
#define FTRP(bp) ((char* )(bp) + GET_SIZE(HDRP(bp)) - DSIZE)
/* Given block ptr bp, compute address of next and previous blocks */
#define NEXT_BLKP(bp) ((char *)(bp) + GET_SIZE(((char *)(bp) - WSIZE)))
#define PREV_BLKP(bp) ((char *)(bp) - GET_SIZE(((char *)(bp) - DSIZE)))
/* Heap prologue block pointer*/
static void *heap_listp;
创建初始空闲链表(init)
mm_init
函数创建一个初始的堆,分配序言块和尾块,并且将其扩展,创建一个大小为4096字节的初始堆区。
/*
* mm_init - initialize the malloc package.
*/
int mm_init(void)
{
/* Create the initial empty heap */
if ((heap_listp = mem_sbrk(4*WSIZE)) == (void *)-1)
return -1;
PUT(heap_listp, 0); /* Alignment padding */
PUT(heap_listp + (1*WSIZE), PACK(DSIZE, 1)); /* Prologue header */
PUT(heap_listp + (2*WSIZE), PACK(DSIZE, 1)); /* Prologue footer */
PUT(heap_listp + (3*WSIZE), PACK(0, 1)); /* Epilogue header */
heap_listp += (2*WSIZE);
/* Extend the empty heap with a free block of CHUNKSIZE bytes */
if (extend_heap(CHUNKSIZE/WSIZE) == NULL) {
return -1;
}
return 0;
}
/* extend_heap -- Extend heap */
static void *extend_heap(size_t words)
{
char *bp;
size_t size;
/* Allocate an even number of words to maintain alignment */
size = (words % 2) ? (words+1) * WSIZE : words * WSIZE;
if ((long)(bp = mem_sbrk(size)) == -1)
return NULL;
/* Initialize free block header/footer and the epilogue header */
PUT(HDRP(bp), PACK(size, 0)); /* Free block header */
PUT(FTRP(bp), PACK(size, 0)); /* Free block footer */
PUT(HDRP(NEXT_BLKP(bp)), PACK(0, 1)); /* New epilogue header */
/* Coalesce if the previous block was free */
return coalesce(bp);
}
释放和合并块(free)
mm_free
释放一个块就是简单地把它的头部和尾部块设置为空闲即可,大小为这个块的大小,并且进行空闲块合并的操作。
/*
* mm_free - Freeing a block and coalesce free block if can .
*/
void mm_free(void *ptr)
{
size_t size = GET_SIZE(HDRP(ptr));
PUT(HDRP(ptr), PACK(size, 0));
PUT(FTRP(ptr), PACK(size, 0));
coalesce(ptr);
}
/* coalesce -- Coalesce free block */
static void *coalesce(void *ptr)
{
size_t prev_alloc = GET_ALLOC(FTRP(PREV_BLKP(ptr)));
size_t next_alloc = GET_ALLOC(HDRP(NEXT_BLKP(ptr)));
size_t size = GET_SIZE(HDRP(ptr));
if (prev_alloc && next_alloc) {
/* Case 1 */
return ptr;
} else if (prev_alloc && !next_alloc) {
/* Case 2 */
size += GET_SIZE(HDRP(NEXT_BLKP