## 关于示例代码的理解 #### 环境 * 从 `mem_init` 中可以看出整个分配器是在一大段已经分配好的连续的内存中进行的 * 同样每次 `mem_sbrk` 进行扩展时也是连续扩展,模拟堆的向上增长 #### 对齐 * 标准 `malloc` 也是八字节对齐,所以可以满足强制八字节对齐的要求 * 返回的内存应为有效载荷部分,如果每个块加上头部和脚部,为了满足对齐要求,须在开头空出四字节的位置 * 也意味着空闲块最小为八字节大小,有效载荷也应八字节对齐,已分配块最小十六字节 #### 测试 * handout 里的测试用例偏少,从网上找到了更为详尽的 `traces` 测试用例来测试 ## version 1 #### 规则与注意 理解逻辑后,接下来将实现自己的版本 * 为方便理解,定义了两个类型别名 ```c typedef unsigned long word; typedef char byte; ``` * 不同于示例代码用序言块和尾块标记,本人仅用两个指针标记头尾,来提高内存利用率 ```c // mark the front and tail pos void *front_p = NULL; void *tail_p = NULL; ``` * 但会增加代码的复杂度,需着重维护 * 同时还应注意若 `bp == front_p` 则 `PREV(bp)` 内的值无效,`tail_p` 同理 * 为保持一致性,向辅助函数内传入的 `size` 均应在传入之前对齐,均不包含头尾部大小 * 仅在内部碎片大于等于十六字节时才进行切割 * 其他部分与示例大同小异 #### bug 与 debug * `#debug`对于 `segmentation fault` 使用 `gdb` 获取头尾块的 `size` 发现尾部异常值 `0xcdcdcd`,在代码中使用 `print` 跟踪 `trail_p` 变量,发现在`__coalesce_next`处没有及时更新 * `#bug1` 若记录的 `size` 是有效载荷的 `size`,合并和分割时应注意增减 `DSIZE` * `#bug2` 每次合并都需要判断 `tail_p` 是否改变,特别是 `__coalesce_next` 的情况 #### 得分 * 隐式空闲链表,首次适配,立即合并 ```c Results for mm malloc: trace valid util ops secs Kops 0 yes 99% 5694 0.007579 751 1 yes 100% 5848 0.006639 881 2 yes 99% 6648 0.010560 630 3 yes 100% 5380 0.008016 671 4 yes 100% 14400 0.000102140762 5 yes 92% 4800 0.006677 719 6 yes 92% 4800 0.005988 802 7 yes 55% 12000 0.141468 85 8 yes 51% 24000 0.274197 88 9 yes 33% 14401 0.128358 112 10 yes 50% 14401 0.002138 6734 Total 79% 112372 0.591722 190 Perf index = 47 (util) + 13 (thru) = 60/100 ``` ## Version 1.1 #### 针对 `realloc` 的优化 v1 * 若 `new_size <= old_size` 则不分配而是切割 * 若下一块未分配且总和大于 `new_size` 则合并 * 若合并后内部碎片过大则仍需分割 * 提高六分,但判断过多,且考虑不全 #### 针对 `realloc` 的优化 v2 * 评估前后空闲块的总大小,若足够,则合并 * 合并不会破坏数据,合并后复制数据,再根据需要分割 * 然而针对第九项测试,合并前后空闲块反而内存利用率低于仅合并后空闲块 #### 针对 `realloc` 的优化 v3 * 最终选择的是逐步的过程,因为从时间开销上来看,直接返回优于仅合并后部分优于合并前后部分,但同时合并前后部分与再分配一段内存的优劣不好比较 * 现在的问题是在仅合并后部分和重新分配之间要不要插一段合并前后部分的条件,两者分数相同,个人认为插入这个条件通用性更好 * 最终的 `realloc` ```c /** * implemented simply in terms of mm_malloc and mm_free * compare adjust_size and total_size step by step */ void *mm_realloc(void *ptr, size_t size) { if (ptr == NULL) return mm_malloc(size); if (size == 0) return NULL; void *new_ptr; size_t adjust_size = ALIGN(size); size_t old_size = SIZE(ptr); if (adjust_size <= old_size) { // just return, for the memory lost is little return ptr; } size_t next_size = (ptr != tail_p && !ALLOC(NEXT(ptr))) ? SIZE(NEXT(ptr)) + DSIZE : 0; size_t total_size = old_size + next_size; if (adjust_size <= total_size) { __coalesce_next(ptr); _place(ptr, adjust_size); // just cut return ptr; } size_t prev_size = (ptr != front_p && !ALLOC(PREV(ptr))) ? SIZE(PREV(ptr)) + DSIZE : 0; total_size += prev_size; if (adjust_size <= total_size) { // coalesce prev or all new_ptr = _coalesce(ptr); memmove(new_ptr, ptr, old_size); _place(new_ptr, adjust_size); } else { if ((new_ptr = mm_malloc(size)) == NULL) return NULL; memmove(new_ptr, ptr, old_size); mm_free(ptr); } return new_ptr; } ``` * 然而此版不适合之后的显示链表 ~~pity because it's elegant~~ #### 得分 ```c Results for mm malloc: trace valid util ops secs Kops 0 yes 99% 5694 0.007401 769 1 yes 100% 5848 0.006883 850 2 yes 99% 6648 0.011138 597 3 yes 100% 5380 0.008327 646 4 yes 100% 14400 0.000092156013 5 yes 92% 4800 0.006244 769 6 yes 92% 4800 0.005888 815 7 yes 55% 12000 0.142196 84 8 yes 51% 24000 0.277304 87 9 yes 50% 14401 0.018129 794 10 yes 86% 14401 0.000132108933 Total 84% 112372 0.483734 232 Perf index = 50 (util) + 15 (thru) = 66/100 ``` ## Version 1.2 #### 使用 `next fit` * 因为大块的空闲块总是趋向于在后面,所以下一次适配不用从头遍历,可以大幅提高吞吐率 * 引入新的全局变量 `fitted_p` 并且需要在多处维护:初始化,分配,合并 #### 结合 `best fit` * `next_fit` 会降低内存利用率,而 `best_fit` 会降低吞吐率,但我们可以进行一个折衷,即在 `fitted_p` 后部分首次适配,而在 `fitted_p` 前部分最佳适配 * 而最佳适配仅需找到满足要求的最小值即可 * 然而因为前面的内存碎片太多,测试下来内存利用率的确有所提高,但是吞吐率下降的却更多,所以最后还是选择了 `_next_fit` #### 得分 ```c Results for mm malloc: trace valid util ops secs Kops 0 yes 91% 5694 0.001803 3158 1 yes 92% 5848 0.001315 4446 2 yes 97% 6648 0.003706 1794 3 yes 97% 5380 0.003602 1494 4 yes 100% 14400 0.000085169213 5 yes 91% 4800 0.004207 1141 6 yes 90% 4800 0.003837 1251 7 yes 55% 12000 0.057487 209 8 yes 51% 24000 0.029497 814 9 yes 50% 14401 0.054370 265 10 yes 70% 14401 0.000116124684 Total 80% 112372 0.160025 702 Perf index = 48 (util) + 40 (thru) = 88/100 ``` ## Version 2 #### 显示 LIFO 空闲链表 * 经过多次尝试,88 分基本上是隐式链表的极限了,为进一步提高吞吐率,我们维护一个双向链表将空闲块串起来,这里选择的则是 LIFO 方式 * 首先由于 32/64 位的地址所需位数不同,为了保持 64 位干净,我们需要定义地址的位数为 `sizeof(unsigned long)`,同时还需定义最小的空闲块大小 * 需要在合并、切割处着重维护显示链表,同时为了减少不必要的判断,本人将选择双向循环列表,并由 `list_p` 标记头部 * 首先,每次调用 `_coalesce` 之前都会产生一个未入链表的空闲块,我们只需要修改 `__coalesce_next, __coalesce_all`,以及没有合并的情况,并将 `list_p` 指向新块 * 初始化时记得将第一个空闲块的头尾指向自己 * 注意插入链表时的顺序,应该先改前后指向自己再改自己指向前后 * 其次,`_place` 时都需要维护链表,存在没有空闲链表的情况,这时让 `list_p` 为空 * 对于 `realloc` 的优化,由于会调用 `_coalesce, _place`,可能会把原始信息破坏,或覆盖链表信息,需要修改为仅合并后部分空闲链表,并且合并部分需重写 * 并入链表的过程~~几乎一样~~不一样,部分可以包装成函数 `_fix_list`实现替换的逻辑,还需要注意加入链表(`__coalesce_none`),移除链表(`_place`)的情况 * 多写了一个 `_check` 函数用以查看遍历空闲链表获得的空闲块数和遍历整个链表获取的空闲块数是否相同,以此判断链表是否正确,并用 `debug` 宏标注,编译选项中加入 `-DDEBUG` 才会输出信息 * 多亏 `_check` 的信息,我们将错误定位在 `__coalesce_none` 处,发现在空闲块存在而没有合并时应写插入的逻辑,而 `_fix_list` 写的是替换的逻辑 * 在解决完一系列 `bug` 后,我们的链表总算是维护好了,但是分数也降到了 80 分,接下来让我们实现显示链表的适配版本 `_first_fit_of_clear` #### 得分 * 可以看到虽然内存利用率有略微下降,但吞吐率却有了一个数量级的提升 ~~那说明辛苦的 `debug` 没有白费~~ * 然而不知道测试分数具体的计算方法,时间下降了一个数量级但是吞吐率分数没变... * 可能原因也在于,少占用内存比少几毫秒时间价值更高吧 ```c Results for mm malloc: trace valid util ops secs Kops 0 yes 90% 5694 0.000211 26960 1 yes 91% 5848 0.000128 45581 2 yes 95% 6648 0.000217 30636 3 yes 98% 5380 0.000135 39911 4 yes 100% 14400 0.000090159645 5 yes 90% 4800 0.000457 10503 6 yes 87% 4800 0.000453 10587 7 yes 55% 12000 0.003927 3056 8 yes 51% 24000 0.002370 10127 9 yes 41% 14401 0.000422 34126 10 yes 86% 14401 0.000087166486 Total 80% 112372 0.008497 13225 Perf index = 48 (util) + 40 (thru) = 88/100 ``` ## 最后 * 所有用到的辅助函数均在开头声明并注释,被淘汰的函数用 `@deprecated` 标记 *** 2022.12.29 ~ 2022.12.31