这是一个本人学习 csapp 的 learning 库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

190 lines
9.9 KiB

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