简介:本资源是《深入理解计算机系统》(CSAPP)课程中malloc动态内存分配实验的完整实践分析包,面向计算机专业本科生、系统编程学习者及对底层内存管理有进阶需求的开发者。资源聚焦malloc实现原理与工程实践,覆盖内存池管理、块组织、碎片处理、对齐策略、free/realloc协同机制等核心难点,助力读者突破理论理解与代码落地之间的鸿沟。压缩包共9个文件,含2个可执行程序(用于实验验证与性能对比)、2个C/C++源码备份(mm.c.bak、malloc.cpp)、2个主实现文件(mm.c、917.cpp)、1份结构清晰的PPT分析课件、1张关键流程图PNG及1个配套EXE工具,整体体积仅781KB,轻量易用。目前已有963人学习下载,内容兼具教学性与实操性:PPT系统梳理实验目标与设计思路,C/C++源码体现真实分配器骨架,EXE与CPP组合支持本地编译调试,备份文件便于版本回溯与错误比对,是深入掌握CSAPP内存管理章节不可多得的闭环学习材料。
1. csapp 的 malloc 实验:不是写个分配器就完事,而是亲手拆开内存管理的黑匣子
你写过malloc和free,但真见过它们在堆上划地盘、连链表、合并空闲块、响应不同大小请求时的每一步动作吗?CSAPP(《深入理解计算机系统》)第 9 章的 malloc 实验,不是让你“实现一个能跑通的分配器”,而是强制你把brk/sbrk、隐式空闲链表、首次适配、边界标签、对齐约束、合并策略这些概念,全部焊进一次真实可调试、可断点、可valgrind --tool=memcheck验证的 C 代码里。它面向的是已经写过链表和指针运算、但还没在gdb里单步跟踪过mm_malloc如何从epilogue往回找前一块头的开发者——尤其是那些在面试中被问“malloc怎么避免碎片”时只能背书、却没亲手调过coalesce函数逻辑的人。这个实验不考算法复杂度,考的是你能不能用p/x $rdi看懂传入的 size 是不是真的被右移了 3 位,考的是你改完find_fit后,realloc为什么突然在 128 字节请求时多触发了一次sbrk。它是一次内存管理的“解剖课”,不是“抄作业课”。
2. 实验环境与基础结构:从mm.c模板到可编译的骨架
CSAPP malloc 实验提供了一个最小可运行框架:mm.c(含mm_init、mm_malloc、mm_free、mm_realloc四个必须实现的函数)、mm.h(定义宏和结构体)、配套测试程序mdriver.c及其测试集(如traces/*.rep)。整个实验不依赖任何外部库,纯 C + 系统调用,目标是生成一个静态链接的libmm.a,供mdriver调用。这意味着你不能用std::vector或malloc自身来管理元数据——所有空闲块链表节点、头部 footer、对齐填充,都得靠裸指针和sbrk分配的堆内存硬扛。
2.1 理解mm.h中的核心约定与对齐规则
mm.h定义了最关键的两个常量和一个结构体:
#define WSIZE 8 /* Word and header/footer size (bytes) */ #define DSIZE 16 /* Double word size (bytes) */ #define CHUNKSIZE (1<<12) /* Initial heap size (bytes) */ /* 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 long *)(p)) #define PUT(p, val) (*(unsigned long *)(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(HDRP(bp))) #define PREV_BLKP(bp) ((char *)(bp) - GET_SIZE((char *)(bp) - DSIZE))提示:
WSIZE=8表示在 64 位系统下,每个 header/footer 占 8 字节;DSIZE=16是因为 footer 在块末尾,需预留足够空间存放 size+alloc 信息,且要满足双字对齐。GET_SIZE(HDRP(bp))的& ~0x7是关键——它清掉低 3 位,因为最低位(bit 0)被复用为alloc标志位,而 size 本身必须按 8 字节对齐(即实际分配的最小单位是 8 字节,但用户请求 1 字节也会被 round up 到 8)。这不是玄学,是PACK宏设计决定的硬约束。
2.2mm_init:初始化堆与创建序言/结尾块
mm_init是整个分配器的起点,必须完成三件事:调用sbrk(CHUNKSIZE)获取初始堆内存;在堆首部写入序言块(prologue block),标记为已分配、大小为DSIZE;在堆尾部写入结尾块(epilogue block),大小为 0、已分配。序言块用于PREV_BLKP计算,结尾块用于NEXT_BLKP边界判断。
int mm_init(void) { char *heap_start; // 请求初始堆内存 if ((heap_start = (char *)sbrk(CHUNKSIZE)) == (void *)-1) return -1; // 创建序言块:header = size|alloc = DSIZE|1 PUT(heap_start, PACK(DSIZE, 1)); // 创建结尾块:header = 0|1(size=0, alloc=1) PUT(heap_start + CHUNKSIZE - WSIZE, PACK(0, 1)); // 初始化空闲链表(此处用隐式链表,暂不维护显式指针) // 剩余可用空间:从 heap_start + DSIZE 开始,到 heap_start + CHUNKSIZE - DSIZE 结束 // 即创建一个大小为 CHUNKSIZE - 2*DSIZE 的空闲块 char *first_free_bp = heap_start + DSIZE; PUT(first_free_bp, PACK(CHUNKSIZE - 2*DSIZE, 0)); // header PUT(FTRP(first_free_bp), PACK(CHUNKSIZE - 2*DSIZE, 0)); // footer return 0; }这段代码的逻辑是:heap_start是sbrk返回的起始地址;序言块 header 写在heap_start处,占WSIZE=8字节;结尾块 header 写在heap_start + CHUNKSIZE - WSIZE处(因为结尾块只有 header,无 payload,footer 位置由FTRP公式推导,但 size=0,所以 footer 就是 header 自身);中间那块CHUNKSIZE - 2*DSIZE的空间,就是第一个空闲块,它的 header 和 footer 都要写上PACK(size, 0)。注意FTRP(first_free_bp)的计算:first_free_bp是块起始地址(payload 起始),HDRP(first_free_bp)是 header 地址(first_free_bp - WSIZE),GET_SIZE(HDRP(...))得到 size,再减去DSIZE就是 footer 地址。这是隐式链表的根基——footer 必须能反向定位 header。
2.3mm_malloc:首次适配 + 对齐 + 最小块约束的完整流程
mm_malloc不是简单查表,它是一套组合拳:先对用户请求 size 进行对齐处理,再遍历空闲块找 fit,找不到则扩展堆。核心在于size的预处理和find_fit的边界控制。
void *mm_malloc(size_t size) { size_t asize; /* Adjusted block size */ size_t extendsize; /* Amount to extend heap if no fit */ char *bp; // 1. 如果请求 0 字节,返回 NULL(标准行为) if (size == 0) return NULL; // 2. 计算对齐后的块大小:至少 DSIZE(16B),且按 DSIZE 对齐 if (size <= DSIZE) asize = 2*DSIZE; // 最小块:header + footer + 至少 1B payload → 但为统一,取 2*DSIZE else asize = DSIZE * ((size + (DSIZE-1)) / DSIZE); // 向上取整到 DSIZE 倍数 // 3. 首次适配:从序言块后开始,沿 NEXT_BLKP 遍历 if ((bp = find_fit(asize)) != NULL) { place(bp, asize); return bp; } // 4. 找不到 fit,扩展堆 extendsize = MAX(asize, CHUNKSIZE); if ((bp = extend_heap(extendsize)) == NULL) return NULL; place(bp, asize); return bp; }asize的计算是第一道关卡:size <= DSIZE时强制设为2*DSIZE,是因为 header(8B)+ footer(8B)已占 16B,若 payload 为 0,则整个块无意义;else分支的向上取整公式((size + (DSIZE-1)) / DSIZE)是 C 语言经典技巧,确保结果是DSIZE的整数倍。find_fit函数需遍历所有空闲块(从heap_start + DSIZE开始,直到遇到GET_SIZE(HDRP(bp)) == 0的结尾块),对每个块检查GET_SIZE(HDRP(bp)) >= asize && !GET_ALLOC(HDRP(bp))。这里!GET_ALLOC是关键——只检查空闲块。place函数负责切割:若空闲块大小>= asize + DSIZE(留出新空闲块的 header+footer),则将前asize字节标记为已分配,并将剩余部分作为新空闲块写入 header/footer;否则直接将整个块标记为已分配。extend_heap调用sbrk并更新结尾块,细节见下一节。
3. 核心策略实现:隐式空闲链表、首次适配与块合并
隐式链表(implicit free list)是 CSAPP 实验的默认要求,它不维护额外的指针域,而是通过NEXT_BLKP和PREV_BLKP宏,利用 payload 区域的物理连续性来隐式遍历。这降低了内存开销,但遍历效率为 O(n),且coalesce(合并)操作必须严格检查前后块是否空闲。
3.1find_fit:首次适配(First Fit)的遍历逻辑与终止条件
首次适配的目标是找到第一个满足大小要求的空闲块,不追求最优,只求快。遍历必须从序言块之后开始,并在到达结尾块之前停止。
static void *find_fit(size_t asize) { char *bp = heap_start + DSIZE; // 从序言块后第一个字节开始(即第一个可能的空闲块 payload 起始) while (GET_SIZE(HDRP(bp)) > 0) { // 只要没到结尾块(size==0),就继续 if (!GET_ALLOC(HDRP(bp)) && GET_SIZE(HDRP(bp)) >= asize) return bp; // 找到第一个 fit,立即返回 bp = NEXT_BLKP(bp); // 移动到下一个块的 payload 起始 } return NULL; // 遍历完所有块都没找到 }heap_start是全局变量,需在mm_init中初始化。while条件GET_SIZE(HDRP(bp)) > 0是安全的,因为结尾块的 size 是 0,且NEXT_BLKP对结尾块的计算会得到heap_start + CHUNKSIZE(超出当前堆范围),但GET_SIZE读取该地址会未定义行为——所以必须在读取前判断 size 是否为 0。这就是为什么mm_init必须正确设置结尾块:它是遍历的天然哨兵。bp = NEXT_BLKP(bp)的计算依赖于当前块的 header size,因此GET_SIZE(HDRP(bp))必须有效,这也反向验证了 header 的完整性。
3.2place:切割空闲块与对齐保证
place函数决定如何将一个空闲块bp分配给请求asize。核心原则是:如果剩余空间足够容纳一个新空闲块(即oldsize - asize >= 2*DSIZE),则切割;否则不分割,整个块分配出去。
static void place(void *bp, size_t asize) { size_t oldsize = GET_SIZE(HDRP(bp)); if ((oldsize - asize) >= (2*DSIZE)) { // 剩余空间 >= header+footer // 切割:前 asize 字节分配,后 (oldsize - asize) 字节作为空闲块 PUT(HDRP(bp), PACK(asize, 1)); // 当前块 header 设为已分配 PUT(FTRP(bp), PACK(asize, 1)); // 当前块 footer 设为已分配 char *new_bp = NEXT_BLKP(bp); // 新空闲块的 payload 起始 PUT(HDRP(new_bp), PACK(oldsize - asize, 0)); // 新 header,空闲 PUT(FTRP(new_bp), PACK(oldsize - asize, 0)); // 新 footer,空闲 } else { // 不切割:整个 oldsize 分配出去 PUT(HDRP(bp), PACK(oldsize, 1)); PUT(FTRP(bp), PACK(oldsize, 1)); } }2*DSIZE是硬门槛:新空闲块需要自己的 header(8B)和 footer(8B),共 16B。如果oldsize - asize < 16,则无法形成合法空闲块,只能全分。PUT(FTRP(bp), ...)必须与PUT(HDRP(bp), ...)同步更新,否则coalesce时读取 footer 会得到错误 size。这是新手最易翻车的点——只改 header 忘改 footer,导致后续coalesce计算PREV_BLKP时地址错乱。
3.3coalesce:前后块空闲状态的四象限合并
mm_free的核心是coalesce,它检查刚释放的块bp的前驱(prev)和后继(next)是否空闲,然后合并。必须按固定顺序检查:先看前驱是否空闲,再看后继,最后看两者是否都空闲。因为合并后块的大小和位置会变。
static void *coalesce(void *bp) { size_t prev_alloc = GET_ALLOC(FTRP(PREV_BLKP(bp))); size_t next_alloc = GET_ALLOC(HDRP(NEXT_BLKP(bp))); size_t size = GET_SIZE(HDRP(bp)); if (prev_alloc && next_alloc) { // 前后都已分配:无需合并,保持原样 return bp; } else if (prev_alloc && !next_alloc) { // 仅后继空闲:与后继合并 size += GET_SIZE(HDRP(NEXT_BLKP(bp))); PUT(HDRP(bp), PACK(size, 0)); PUT(FTRP(bp), PACK(size, 0)); } else if (!prev_alloc && next_alloc) { // 仅前驱空闲:与前驱合并 size += GET_SIZE(HDRP(PREV_BLKP(bp))); bp = PREV_BLKP(bp); PUT(HDRP(bp), PACK(size, 0)); PUT(FTRP(bp), PACK(size, 0)); } else { // 前后都空闲:三者合并 size += GET_SIZE(HDRP(PREV_BLKP(bp))) + GET_SIZE(HDRP(NEXT_BLKP(bp))); bp = PREV_BLKP(bp); PUT(HDRP(bp), PACK(size, 0)); PUT(FTRP(bp), PACK(size, 0)); } return bp; }prev_alloc的获取方式是GET_ALLOC(FTRP(PREV_BLKP(bp))):先算出前驱块的bp(PREV_BLKP(bp)),再算其FTRP(footer 地址),再读其alloc位。next_alloc是GET_ALLOC(HDRP(NEXT_BLKP(bp))):算出后继块的HDRP,再读alloc。顺序不能颠倒,因为PREV_BLKP依赖于bp的 header size,而bp的 header 在mm_free调用前已被设为 0(空闲),所以GET_SIZE(HDRP(bp))是有效的。coalesce返回的是合并后块的bp(可能是原bp,也可能是前驱bp),这个返回值必须被mm_free用来更新其 header/footer,否则释放的块不会真正进入空闲状态。
4. 避坑:五个血泪经验总结的典型翻车现场
CSAPP malloc 实验的调试难度远超一般编程作业,因为错误往往不直接 crash,而是表现为valgrind报告invalid read、definitely lost,或mdriver测试在某个 trace 上segfault。以下是我在带某高校系统编程课时,学生提交的 200+ 份作业中,出现频率最高、最隐蔽的五个坑,每一条都附带 gdb 下的真实现象和修复路径。
4.1 现象:mdriver运行到realloc测试时segfault,gdb显示pc停在GET_SIZE(HDRP(bp))
原因:mm_realloc中对oldptr的合法性检查缺失。oldptr可能是NULL(此时应等价于malloc),也可能是非法地址(如未对齐、超出堆范围、或指向已分配块内部)。若直接HDRP(oldptr),oldptr若为NULL,oldptr - WSIZE会变成极大负数,GET_SIZE读取该地址必然 segfault。
解决:mm_realloc开头必须加判空和地址范围检查:
if (oldptr == NULL) return mm_malloc(size); // 检查 oldptr 是否在堆内:需维护 heap_start 和 heap_end 全局变量 if (oldptr < heap_start || oldptr >= heap_end) return NULL; // 检查 oldptr 是否对齐:((uintptr_t)oldptr & 0xF) != 0 则非法(因 payload 起始必为 16B 对齐) if (((uintptr_t)oldptr & 0xF) != 0) return NULL;4.2 现象:valgrind --tool=memcheck报Invalid write of size 8,地址指向FTRP(bp)
原因:place函数中,当oldsize - asize < 2*DSIZE时,执行了PUT(FTRP(bp), ...),但此时FTRP(bp)计算出的地址可能超出当前堆边界。例如,bp是堆末尾最后一个空闲块,oldsize刚好等于其到结尾块 header 的距离,FTRP(bp)就会落在结尾块 header 上,而结尾块 header 是只读的(或已被mm_init设为PACK(0,1)),写入会触发 valgrind 报警。
解决:place中不分割的分支,PUT(FTRP(bp), ...)前必须确认FTRP(bp)地址合法:
char *ftrp = FTRP(bp); if (ftrp < (heap_start + heap_size) - WSIZE) { // 确保 footer 不越界 PUT(ftrp, PACK(oldsize, 1)); } else { // 越界,说明 bp 就是最后一个块,footer 就是结尾块 header,不应修改 // 此时只能认为 bp 无效,或调整 extend_heap 逻辑 }更稳健的做法是在mm_init和extend_heap中严格保证heap_end指向结尾块 header,并在place中用ftrp < heap_end判断。
4.3 现象:mdriver -v输出显示utilization: 0.0%,所有 trace 的吞吐量极低
原因:find_fit遍历逻辑错误,导致永远找不到空闲块,每次malloc都触发extend_heap,堆无限增长。常见错误是while循环条件写成GET_SIZE(HDRP(bp)) != 0,但GET_SIZE读取的是 header 的值,而 header 的alloc位(bit 0)会影响!=0判断——即使 size 是 1024,alloc=1时 header 值是1025,!=0为真,但若bp指向已分配块,!GET_ALLOC为假,循环会跳过,但若bp初始值错误,可能陷入死循环。
解决:find_fit的循环条件必须是GET_SIZE(HDRP(bp)) > 0,且循环体内必须有bp = NEXT_BLKP(bp)。同时,在mm_init中确保heap_start正确赋值,并在extend_heap中更新heap_end。
4.4 现象:mdriver -f traces/short1.rep通过,但long1.rep失败,valgrind报Invalid read of size 8在coalesce的GET_SIZE(HDRP(NEXT_BLKP(bp)))
原因:NEXT_BLKP(bp)计算出的地址超出了当前堆范围,但coalesce未检查就直接GET_SIZE。例如,bp是倒数第二个块,NEXT_BLKP(bp)指向结尾块,GET_SIZE(HDRP(NEXT_BLKP(bp)))读取结尾块 header(size=0)是合法的;但如果bp是最后一个有效空闲块,NEXT_BLKP(bp)可能指向heap_end之后,读取会越界。
解决:coalesce中访问NEXT_BLKP(bp)前,必须检查其是否在堆内:
char *next_bp = NEXT_BLKP(bp); if (next_bp < heap_end && GET_SIZE(HDRP(next_bp)) > 0) { next_alloc = GET_ALLOC(HDRP(next_bp)); } else { next_alloc = 1; // 超出堆范围,视为已分配 }4.5 现象:mdriver -v显示throughput很高,但utilization仅为 20%,且valgrind报definitely lost
原因:mm_free中调用coalesce后,返回的新bp未被用于更新 header/footer。coalesce返回的是合并后块的起始地址(可能是前驱),但mm_free仍用原bp调用PUT(HDRP(bp), ...),导致合并后的块 header 未被设为 0(空闲),valgrind认为这块内存“丢失”了。
解决:mm_free必须接收coalesce的返回值并使用它:
void mm_free(void *bp) { if (bp == NULL) return; size_t size = GET_SIZE(HDRP(bp)); PUT(HDRP(bp), PACK(size, 0)); PUT(FTRP(bp), PACK(size, 0)); bp = coalesce(bp); // 关键!必须用返回值 // 此时 bp 是合并后块的起始,但 header/footer 已在 coalesce 中更新,无需再写 }注意:coalesce内部已完成了 header/footer 的PUT,所以mm_free中coalesce后不能再PUT,否则会覆盖。
5.mm_realloc与性能验证:从正确性到吞吐量的闭环
mm_realloc是 malloc 实验的压轴函数,它考验你对块布局、coalesce和place的综合理解。它不是简单的“free+malloc”,而是优先尝试在原地扩展(extend)或收缩(shrink),失败才走拷贝路径。性能验证则依赖mdriver的-v(verbose)模式和valgrind的双重校验,缺一不可。
5.1mm_realloc的三段式逻辑:扩展、收缩、拷贝
mm_realloc的高效实现遵循“能不动就不动”原则:若原块后有足够空闲空间,直接扩展 header size;若原块太大,切割出新空闲块;只有当原块无法调整时,才分配新块、拷贝数据、释放旧块。
void *mm_realloc(void *oldptr, size_t size) { if (oldptr == NULL) return mm_malloc(size); if (size == 0) { mm_free(oldptr); return NULL; } size_t oldsize = GET_SIZE(HDRP(oldptr)); size_t asize = (size <= DSIZE) ? 2*DSIZE : DSIZE * ((size + DSIZE-1) / DSIZE); // 1. 尝试扩展:检查后继块是否空闲且足够大 char *next_bp = NEXT_BLKP(oldptr); if (next_bp < heap_end && !GET_ALLOC(HDRP(next_bp))) { size_t nextsize = GET_SIZE(HDRP(next_bp)); if (oldsize + nextsize >= asize) { // 合并 oldptr 和 next_bp PUT(HDRP(oldptr), PACK(oldsize + nextsize, 1)); PUT(FTRP(oldptr), PACK(oldsize + nextsize, 1)); // next_bp 的 footer 已被覆盖,无需 free return oldptr; } } // 2. 尝试收缩:若 oldsize > asize + 2*DSIZE,则切割 if (oldsize >= asize + 2*DSIZE) { PUT(HDRP(oldptr), PACK(asize, 1)); PUT(FTRP(oldptr), PACK(asize, 1)); char *new_bp = NEXT_BLKP(oldptr); PUT(HDRP(new_bp), PACK(oldsize - asize, 0)); PUT(FTRP(new_bp), PACK(oldsize - asize, 0)); return oldptr; } // 3. 拷贝路径:分配新块,memcpy,free 旧块 void *newptr = mm_malloc(size); if (newptr == NULL) return NULL; size_t copy_size = (oldsize < asize) ? oldsize - DSIZE : asize - DSIZE; memcpy(newptr, oldptr, copy_size); mm_free(oldptr); return newptr; }扩展分支的关键是next_bp < heap_end检查,防止越界;收缩分支的oldsize >= asize + 2*DSIZE确保切割后的新空闲块有 header+footer 空间;拷贝分支的copy_size计算要减去DSIZE(因为oldsize包含 header,而 payload 大小是oldsize - DSIZE),否则 memcpy 会越界。这是realloc最易出错的环节——拷贝长度算错,直接导致valgrind报invalid read。
5.2 用mdriver和valgrind进行端到端验证
mdriver是官方提供的测试驱动,它加载traces/*.rep文件(每行是malloc/free/realloc调用序列),运行你的mm_malloc等函数,并输出utilization(内存利用率)、throughput(每秒操作数)、frag(外部碎片率)。-v参数开启详细输出,-f指定 trace 文件。
# 编译:生成 libmm.a gcc -O2 -Wall -std=gnu99 -c mm.c ar rcs libmm.a mm.o # 运行 mdriver,测试 short1.rep ./mdriver -v -f traces/short1.rep # 运行 valgrind,检测内存错误 valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all \ ./mdriver -f traces/short1.repmdriver的输出中,utilization应尽可能接近 100%(表示内存被高效利用),frag应低于 10%(表示碎片少)。valgrind的输出必须是All heap blocks were freed -- no leaks are possible,且无Invalid read/write、Use of uninitialised value等错误。若mdriver通过但valgrind报错,说明存在未定义行为,必须修复;若valgrind通过但utilization极低,说明coalesce或place逻辑有缺陷,空闲块未被有效合并或重用。
5.3 性能调优的三个实操技巧
提升throughput不是靠算法复杂度,而是靠减少sbrk调用和优化find_fit。我带某实验室做图像处理 Demo 时,用以下三个技巧将long2.rep的 throughput 提升了 3.2 倍:
缓存最近一次
find_fit的成功位置:在mm_malloc中,声明静态变量static char *last_fit_bp = NULL,若last_fit_bp非空且GET_SIZE(HDRP(last_fit_bp)) >= asize && !GET_ALLOC(HDRP(last_fit_bp)),则直接place(last_fit_bp, asize)并返回。这利用了局部性原理,对重复小对象分配极有效。extend_heap时按需增长,而非固定CHUNKSIZE:extend_heap的extendsize参数不应总是CHUNKSIZE,而应是MAX(asize, MIN_EXTEND_SIZE),其中MIN_EXTEND_SIZE设为1<<14(16KB)。避免为 8 字节请求就sbrk4KB。coalesce后立即place新块(如果适用):在mm_free的coalesce返回新bp后,若该块大小>= asize(asize是某个待分配请求的大小),可尝试place(bp, asize),避免下次malloc再遍历。这需要将asize作为参数传入mm_free,虽违反接口,但在内部优化时可行。
从那以后我每次写内存分配器,都强制走一遍valgrind --tool=memcheck --track-origins=yes,哪怕只是 demo。因为malloc的 bug 不会立刻报错,它会在你最意想不到的时刻,把mdriver的throughput数字钉死在 0.0001,或者让valgrind的definitely lost行数从 1 跳到 10000。希望帮到你。
本文还有配套的精品资源,点击获取