浅谈 tcmalloc 设计

https://developer.aliyun.com/article/6045#slide-5
查阅了很多资料, 这一篇是写得最好的. 参考了许多这里的内容.

之前介绍了 ptmalloc 的设计, 现在来看看 ptmalloc 仍然存在哪些缺点.

  1. 性能差, 每次分配都要去抢占一个 arena, 涉及锁, 互斥锁开销大, 自旋锁CPU.
  2. 释放内存必须从每个 arena 的 top-chunk 开始, 容易导致长周期小内存卡住整个 arena 无法回收内存.
  3. 大小不一的 chunk 连成链表, 容易造成内存碎片, 同时 chunk 的额外开销较大 (每个至少 8B)
  4. 多个 arena 互相隔离, 有内存浪费的风险.

tcmalloc 整体设计

tcmalloc 设计的出发点主要是快.
tcmalloc 使用 PageHeap, CentralCache, ThreadCache 三级结构来管理内存, 设计其实也不复杂, 简而言之:

  • PageCache: 负责向系统申请内存, 并用 span 组织起来 (1个span是若干页连续的内存).
  • CentralCache: 管 PageCache 要 span, 并将 span 拆成 object, 组织成若干个 CentralFreeList 准备分配给 ThreadCache 用.
  • ThreadCache: 类似 ptmalloc 的 bin/chunk, 用 freelist/object 来组织各级对象, 向 CentralCache 要资源.

从这里就可以窥见 tcmalloc 相比 ptmalloc 优越的地方了:

  • ThreadCache 每个线程一个, 独占, 普通的分配释放不需要加锁.
  • 归还内存以 span 为单位, 不存在 ptmalloc 中 topchunk 卡住的问题.
  • span 将申请来的内存分块管理, 而不是 ptmalloc 那样一个 arena 一整块, 有助于分治管理和减少内存碎片, 并且实际内存块不需要存储额外头尾信息, 由数据结构统一管理, 减少额外开销.
  • 内存资源都来自于 PageHeap, 不存在 arena 隔离导致资源浪费的问题.

组件设计

ThreadCache

普普通通, 就是各种 size 的若干个 object 链表(freelist).

CentralCache

包含各种 size 的若干个 freelist, 不过这个 freelist 和 ThreadCache 的 freelist 结构不太一样.
CentralCache 的 freelist 由两个链表组成, non-empty-span 和 empty-span, 每个 span 倒是长得跟 ThreadCache 的 freelist 差不多, 挂着若干个可用 object.
之所以 CentralCache 的 freelist 管理 span 而不直接管理 object, 目的是方便不用的时候将整个 span 返回给 PageHeap. 不然就不知道要来的 span 的使用情况了.
同时 CentralCache 的 freelist 还维护了一个 tc_slots_, 理由是 object 返还时需要找到相应的 span, 这个过程比较耗时, 先直接不分 span 地返回到 tc_slots_, 之后批量丢回 span, 可以提高效率.

PageHeap

负责向系统要内存, 组织成 span.
和 sizeclass 对应的 freelist 组织模式比较类似, PageHeap 组织若干个 span 链表, 每个链表里的 span 都是相同大小 (1 ~ 256页).
较大的 span 用一个 large-span 链表单独管理, 不拆成 object.
除此之外, PageHeap 还维护了一个 Page->Span 的映射, 实现是 RadixTree. 这是因为 object 不像 ptmalloc 里的 chunk 记了 header/tail, 我们要知道 object 属于哪个 span, 就只有靠地址对齐映射来, 这也是 tcmalloc 比 ptmalloc 更省内存的一个关键法宝.

tcmalloc 工作机制

申请小内存

  1. 根据需要的内存大小, 映射到合适类型的 object.
  2. 到 ThreadCache 的相应 freelist 中看是否有可用 object, 有就直接取出来用, 申请完毕.
  3. 没有就去 CentralCache 的相应 CentralFreeList 找到合适的 span(non_empty), 看看 span 下是否有可用 object, 有就取几个到 ThreadCache 中用, 申请完毕.
  4. 没有就去 PageHeap 中要几页内存 (span), 拆成需要的 object, 放到 CentralCache 中, 再取几个给自己的 ThreadCache, 申请完毕.
  5. PageHeap 也没有, 就向系统要几页内存, 组织成 span.

申请大内存(>256K)

直接管 PageHeap 要一个刚好大于等于所需内存数量的 span 直接用, 不拆成 object 走上面那套管理.

释放小内存

  1. 直接放到 ThreadCache 里.
  2. 如果 freelist 长度超过阈值或 ThreadCache 容量超过阈值, 则触发回收.
  3. object 被回收到 CentralCache, 可能放到了 tc_slots_, 也可能放到 span, 取决于当时的状态.
  4. 如果回收导致 span 的 refcount 减为 0, 则将其释放回 PageHeap, 这一过程中尝试 span 合并.

释放大内存

直接释放回 PageHeap.

回收策略

ThreadCache 容量超限时就涉及到内存回收, tcmalloc 采取的策略回收掉每个 freelist k/2 的 object, 其中 k 是两次回收间隔时间里, freelist 的最小长度.

freelist 慢启动增长

ThreadCache 对单个 freelist 的长度限制采取慢启动策略, 即每个 freelist 最初长度限额为 1, 1~batch_size 之间为慢启动状态, 之后每次触发超限或不够时都会给限额加 1, 如果长度达到 batch_size, 此后再碰到 freelist 为空会以整数倍进行扩展, 超限也以整数倍缩减.
这个策略保证了低频使用的 freelist 不浪费空间, 同时高频使用的 freelist 能获得一个相对合理的配额.

posted @ 2022-01-13 19:31  erenn  阅读(231)  评论(0)    收藏  举报