实验要求
实现一个堆上的动态内存分配器,达到最高性能
前期准备
- 框架代码是一个最快的、最低效率的malloc,直接运行默认测试集得到66分(以下评分均基于默认测试集)
- 书上给的代码是基于隐式空闲链表,使用立即边界标记合并、首次适配方式的简单分配器,我在CSAPP官网下载了书上的代码,运行得到的分数是75分
-
- 这里有个小插曲,无论是书上的代码还是网上已经实现的代码,直接运行都会显示“段错误”,经过某评论的启发,发现这里的代码应该是32位的,而我的环境是64位的,所以无法直接运行。解决方法如下
- 执行
sudo apt-get install gcc-multilib
完善编译环境 - 在
Makefile
中加上-m32
- 执行
- 根据书上内容,“因为块分配与堆块的总数呈线性关系,所以对于通用的分配器,隐式空闲链表是不合适的”,因此为了提高分配器的评分,我采用了分离适配的空闲链表算法,这种方法既快速,对内存的使用也很有效率
- 这一方法也是C标准库中提供的GNU malloc包采用的算法
算法思路
- 空闲块的header后存储两个指针,分别指向前一个空闲块和后一个空闲块(类似于双向链表)
-
- 分离适配的空闲链表,以2的幂次为界划分成链表数组(数组具体长度需要调参确定最优值),每一个链表内空闲块的大小均按递增的顺序排列,因此可以采用首次适配的方式来选择空闲块
-
- 当应用程序发起内存请求时,分配器首先检查是否有足够大的空闲块可用,如果有,则从相应的空闲链表中取出一个块进行分配;如果没有,它会扩展堆以创建一个足够大的块来满足请求
- 当内存被释放时,释放的块会与相邻的空闲块合并,然后被重新链接到相应的空闲链表中,以保持内存的连续性和提高内存利用效率
优化思路
get_freelisthead
,输出符合size大小的分离的空闲链表的起始位置- 常规的思路是用多个
if
判断语句来确定给定的size大小在哪个链表中 - 稍微进阶一点的写法是利用下面这种三目运算符实现
int i = (size <= 16) ? 0 : (size <= 32) ? 1 : (size <= 64) ? 2 : (size <= 128) ? 3 : (size <= 256) ? 4 : (size <= 512) ? 5 : (size <= 1024) ? 6 : (size <= 2048) ? 7 : (size <= 4096) ? 8 : 9;
- 更好的写法是利用移位运算来实现
- 常规的思路是用多个
int shift = 4;
while (shift < (SEG_LIST_COUNT + 3) && size > (1 << shift))
shift++;
mm_realloc
,重新分配- 常规的思路就是调用
mm_malloc
分配一个新空间,然后使用memcpy
将原块中的数据复制过去,在把原来的块mm_free
,但是其中复制数据的过程其实很浪费时间,如何避免复制数据?- 尽量保证数据不动,若空间不够可以把下一个空闲块拼接起来
- 考虑下面三种情况
- Case1:若原块大小更大,则返回原指针(若剩余空间超过最小块大小,将剩余空间回收)
- Case2:若原块大小更小,考虑下一个块是否为空闲块,若加上下一个空闲块能满足要求,则把两个块拼接起来,返回原指针(若剩余空间超过最小块大小,将剩余空间回收)
- Case3:若原块大小更小,且不能通过拼接下一个块达到要求,只能重新分配一个空间,复制原数据,再释放原空间
- 常规的思路就是调用
实验结果
- 最终的评分是91分,比框架代码提高了37.9%
-
参考链接
CSAPP:Lab5-Malloc Lab - 知乎
六 Malloc Lab - 简书
GitHub - hehozo/Malloc-lab: CSAPP Malloc-lab: 91% performance index, my own dynamic memory allocator in C.
与 Malloc Lab(in csapp) 大战三天三夜纪实 - 知乎
CSapp lab5 MAlloc Lab以及段错误处理 && CSapp第九章学习 2 Linux虚拟内存系统与Linux进程虚拟地址空间 - TheDa - 博客园
CSAPP-malloclab 解题思路记录 - 找一个吃麦旋风的理由
完整代码
// 本实验采用分离适配的空闲链表算法,这是一种高效的内存管理技术,它通过维护多个不同大小的空闲链表来优化内存的分配和回收过程。
// 该算法将内存划分成一系列固定大小的块,这些块可以进一步细分以适应不同大小的内存请求。每个空闲链表根据块的大小进行分类,
// 每个分类中包含相同大小区间的空闲块。当应用程序发起内存请求时,分配器首先检查是否有足够大的空闲块可用,
// 如果有,则从相应的空闲链表中取出一个块进行分配;如果没有,它会扩展堆以创建一个足够大的块来满足请求。
// 当内存被释放时,释放的块会与相邻的空闲块合并,然后被重新链接到相应的空闲链表中,以保持内存的连续性和提高内存利用效率。
// 这种算法的优点在于其快速响应内存请求的能力,并通过合并空闲块减少了内存碎片,从而显著提升了内存的使用效率。
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#ifndef _WIN32
#include <unistd.h>
#endif
#include <string.h>
#include "mm.h"
#include "memlib.h"
/*********************************************************
* 亲们请注意:开始之前,请把下面的信息修改为你的个人信息
********************************************************/
team_t team = {
/* 团队名字 */
"Lane",
/* 团队老大的名字 */
"Lane",
/* 团队老大的email地址 */
"[email protected]",
/* 团队其他成员的名字 (如果没有,就空着) */
"",
/* 团队其他成员的email地址 (如果没有,就空着) */
""};
/* 常数和宏定义 */
#define WSIZE 4 // 字(bytes)
#define DSIZE 8 // 双字(bytes)
#define CHUNKSIZE (1 << 12) // 按此数量(字节)扩展堆
#define SEG_LIST_COUNT 22 // 分离的空闲链表长度(对不同测试集其最优值不同,需要调参)
#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(