所有代码只针对测试数据进行特别优化,可能不具有普适性
代码预览
显式空闲链表+分离适配+realloc_bal 测试点优化+binary_bal 测试点优化
宏定义

宏定义和书本基本相同,但增加了 PRED 和 SUCC 两个宏用作定位空闲块中的前驱指针和后继指针,前驱指针保存在空闲块首起 4 个字节,而后继指针保存在后 4 个字节
函数及全局变量声明

root 指针数组用作分离适配器的空闲链表数组,维护着链表头
MAX_SIZE 为 root 数组的大小
heap_listp 为堆顶指针
根据 2 的幂划分块的大小:{0~8},{9~16},{17~32},…,{2049~4096},{4097~∞}
mm_init 函数

相比于书上代码增加了对 root 的初始化,同时改变初始扩展堆的大小(原为 CHUNKSIZE/WSIZE,现改为 115 字节),经过测试,改后代码能比原代码增加 5 分
extend_heap 函数

和书上代码相同
mm_malloc 函数

和书上代码基本相同,但由于 place 函数由 void 类型改为 void*类型,同时改变形参数量,所以对 place 函数有关代码做出了调整
mm_free 函数

相比于书上代码增加了对于空闲块的前驱指针和后继指针的初始化操作
mm_realloc 函数

ptr 为空时直接调用 mm_malloc 函数,size 为 0 时直接调用 mm_free 函数,否则
首先将输入的 size 进行 8 字节对齐,得到 asize
接着判断 ptr 本身大小是否大于 asize,如果小于等于则直接返回 ptr 指针(抛弃空闲区域),否则判断 ptr 后是否为空闲块且空闲块是最后一块,如果是则判断两者合并后是否放的下,如果放得下则直接更新 ptr 并返回,否则进行堆扩展并更新返回
如果不满足上述所有情况则进行常规 mm_malloc 并进行内存复制
judge 函数

用于判断 size 大小的空闲块应该被放置在哪个空闲链表数组中
place 函数

flag 用于判断调用者来源于 malloc 还是 realloc 函数(realloc 函数无需进行 relink 操作)
对于剩余部分大于 2*DSIZE 的情况,对 asize 进行判断,如果小于 96(实际范围可以在 80~110 之间浮动,分数相同),则将空闲块放在右边,否则则将空闲块放在左边,放置的同时将空闲块的前驱和后继初始化并插入空闲链表中
对于剩余部分小于 2*DSIZE 的情况,抛弃剩余空闲区域
返回指针为分配块所对应指针
coalesce 函数

和书上代码基本类似,只是在对应情况下加上 relink 对应空闲块
最后加上 updatenode 函数更新新的空闲块到对于空闲链表中
find_fit 函数

从对应空闲链表数组中开始寻找,首次适配,如果当前数组中没有找到则跳入下一下标中继续寻找,如果所有空闲块都无法放入则返回 NULL
relink 函数

基础的空闲链表删除节点函数,最后对 bp 进行初始化
updatenode 函数

基础的空闲链表(OrderedList)插入节点函数,链表为升序排列,分头部插入和非头部插入两种情况
优化原理
- 采用显式空闲链表相对于隐式空闲链表,在插入的过程中只需遍历空闲链表而非整个堆,能大大加快速度
- 采用分离适配,将将近大小的块放入一个链表中,类似于链式哈希表,能大大加快检索速度,同时由于优先遍历相近的块,所以相近大小的分配块会被分配到相近大小的空闲块中,提升内存利用率
- 链表采用升序排列,首次适配,所以每次都能适配到最接近大小的空闲块,即最佳适配,能在牺牲一小部分速度的同时大大提升内存利用率
- 针对 realloc_bal 和 realloc2_bal 测试点进行优化,注意到测试点中对同一块区域进行反复 realloc,size 升序排列,同时内部穿插 alloc 和 free,此时若对于 realloc 的块直接向后扩展堆,同时抛弃空闲块,则不会打乱堆中的顺序,使内存利用率上升
- 针对 binary_bal 和 binary2_bal 测试点进行优化,注意到测试点中先反复 alloc 小块和大块,之后一次性 free 大块并 alloc 更大的块,如果不进行优化,则由于之后 alloc 的块并不能被插入到之前 free 的块中(大小不够),所以会造成空间的大量浪费,而选择将大块的空闲块放在左边,小块的空闲块放在右边,当 free 大块时,小块的空闲块,大块的空闲块和大块 free 的空间都能相合并,形成一个更大的空间并能容纳下之后 alloc 的块,能大大提升内存利用率
- 其他优化:改变 mm_init 中初始堆的大小,经测试,当初始堆的大小为 115 字节时有最大内存利用率(仅对测试点进行优化,无普适性)
实验过程及分析
详细注释请见完整代码
- 使用书本上代码(隐式空闲链表)进行首次尝试

可以发现内存的利用率只有 74%,而且速度也很慢 - 随后尝试将隐式改为显式空闲链表(LIFO)

时间方面已经到达满分,之后需要对空间进行进一步优化 - 考虑采用分离适配方法(LIFO),将块大小分解到 131072 大小

内存利用率只有 2%的提升,这也在意料之中,因为分离适配主要提升速度
首先观察到最后 4 个测试点的内存利用率非常低,首先考虑优化最后两个测试点(realloc_bal 和 realloc2_bal) - 通过观察测试点后选择修改 realloc 函数如下


即首先判断 asize 是否和 copySize 相等,如果相等则直接返回 ptr,如果小于则调用 place 函数对分配块进行重新分配,否则则调用 coalesce2 函数(和 coalesce 函数类似),对于原块的前后寻找是否存在空闲块,如果存在则对其进行合并并重新分配,否则则重新 alloc 并内存复制 - 此外修改 updatenode 函数代码,使其变为升序排列,这样就可以配合哈希表达到不损耗太多速度的同时进行最优适配

此时内存利用率有了部分的提升 - 接着考虑对后部空闲块直接扩展堆,即判断
1 | (( !GET_ALLOC(HDRP(NEXT_BLKP(ptr)))&&!GET_SIZE(HDRP(NEXT_BLKP(NEXT_BLKP(ptr)))))) |
- 在修改完代码并确认代码无误后却一直发生段错误

在尝试了许多办法后发现将 CHUNKSIZE 的大小缩减到 256 字节后就不再发生段错误,推测可能是代码之间仍然存在逻辑错误,或是由于测试点的特殊性导致段错误,而将 CHUNKSIZE 进行了修改后正好改变了分配块在内存中的排布顺序,从而消除了段错误 - 同时最后的测试点有了较大幅度的提升,已经到 97%

随后尝试对 binary_bal 和 binary2_bal 测试点进行优化
考虑按照块的大小选择 place 分配方式,即大块分在右边,而小块分在左边,分界线介于 80~110 字节之间 - 但在修改完代码后发现

所有测试点均在第六行发生负载覆盖问题,通过查看测试点可以知道第一次 malloc 可以正常执行,但在进行第二次 malloc 后就覆盖掉了第一次 malloc 的内容 - 调试发现

发现第一次将分配块(0xf697c118)的 ALLOC 置为 1,但在 第二次 malloc 时 ALLOC 被隐式地修改回了 0
经过长时间的调试发现问题在于没有修改传入的 bp 指针,导致 bp 指针指向了错误的位置,而在之后 malloc 中影响到了之前的 ALLOC
所以将 place 函数从 void 类型修改为 void *类型,将修改后的 bp 指针传出,并在 malloc 函数中更新 bp 指针即可消除负载覆盖的问题 - 但这时又再次出现了段错误的问题

经过排查发现是 coalesce2 函数中判断块的前部是否存在空闲块和 place 函数发生了冲突
简单起见,直接暴力删除 coalesce2 中对于 flag==2 的情况,同时将 CHUNKSIZE 修改回 4096 - 修改后最后两个测试点发生 error

- 考虑 coealesce2 函数和其他许多函数可能发生了冲突,最后选择直接删除 coalesce2 整个函数

分数提升到了 92 分
最后通过重整代码和修改 CHUNSIZE 大小和初始堆的大小,成功再次提升 5 分 - 最终实验结果

完整代码
1 | /* |
Leave a comment