这是一个本人学习 csapp 的 learning 库
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.
 
 
 
 
 
 

11 KiB

关于示例代码的理解

环境

  • mem_init 中可以看出整个分配器是在一大段已经分配好的连续的内存中进行的
  • 同样每次 mem_sbrk 进行扩展时也是连续扩展,模拟堆的向上增长

对齐

  • 标准 malloc 也是八字节对齐,所以可以满足强制八字节对齐的要求
  • 返回的内存应为有效载荷部分,如果每个块加上头部和脚部,为了满足对齐要求,须在开头空出四字节的位置
  • 也意味着空闲块最小为八字节大小,有效载荷也应八字节对齐,已分配块最小十六字节

测试

  • handout 里的测试用例偏少,从网上找到了更为详尽的 traces 测试用例来测试

version 1

规则与注意

理解逻辑后,接下来将实现自己的版本

  • 为方便理解,定义了两个类型别名
    typedef unsigned int word;
    typedef char byte;
    
  • 不同于示例代码用序言块和尾块标记,本人仅用两个指针标记头尾,来提高内存利用率
    // mark the front and tail pos
    void *front_p = NULL;
    void *tail_p = NULL;
    
    • 但会增加代码的复杂度,需着重维护
    • 同时还应注意若 bp == front_pPREV(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 的情况

得分

  • 隐式空闲链表,首次适配,立即合并
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
    /**
     * 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

得分

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

得分

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(size_t),同时还需定义最小的空闲块大小
  • 需要在合并、切割处着重维护显示链表,同时为了减少不必要的判断,本人将选择双向循环列表,并由 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 没有白费
  • 然而不知道测试分数具体的计算方法,时间下降了一个数量级但是吞吐率分数没变...
  • 可能原因也在于,少占用内存比少几毫秒时间价值更高吧
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

Version 2.1

最佳适配

  • 可能也是为了鼓励提升内存利用率,而我们的吞吐率已经极大提高了,所以可以用最佳适配来换取内存利用率
  • realloc 也添加了一些情况的判断

得分

  • 我们得到了最终的得分,可以看到牺牲了很多吞吐率才换来了内存利用率百分之三的提升
Results for mm malloc:
trace  valid  util     ops      secs  Kops
 0       yes   99%    5694  0.000145 39405
 1       yes  100%    5848  0.000166 35314
 2       yes  100%    6648  0.000207 32131
 3       yes  100%    5380  0.000140 38374
 4       yes  100%   14400  0.000089162528
 5       yes   95%    4800  0.002331  2059
 6       yes   95%    4800  0.002358  2035
 7       yes   55%   12000  0.027697   433
 8       yes   51%   24000  0.063180   380
 9       yes   40%   14401  0.000257 55970
10       yes   73%   14401  0.000077187513
Total          83%  112372  0.096647  1163

Perf index = 50 (util) + 40 (thru) = 90/100

最后

  • 最终的版本是显示空闲链表LIFO,最佳适配,立即合并
  • 所有用到的辅助函数均在开头声明并注释,被淘汰的函数用 @deprecated 标记

2022.12.29 ~ 2022.12.31