浅谈 tcmalloc 设计
https://developer.aliyun.com/article/6045#slide-5
查阅了很多资料, 这一篇是写得最好的. 参考了许多这里的内容.
之前介绍了 ptmalloc 的设计, 现在来看看 ptmalloc 仍然存在哪些缺点.
- 性能差, 每次分配都要去抢占一个 arena, 涉及锁, 互斥锁开销大, 自旋锁CPU.
- 释放内存必须从每个 arena 的 top-chunk 开始, 容易导致长周期小内存卡住整个 arena 无法回收内存.
- 大小不一的 chunk 连成链表, 容易造成内存碎片, 同时 chunk 的额外开销较大 (每个至少 8B)
- 多个 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 工作机制
申请小内存
- 根据需要的内存大小, 映射到合适类型的 object.
- 到 ThreadCache 的相应 freelist 中看是否有可用 object, 有就直接取出来用, 申请完毕.
- 没有就去 CentralCache 的相应 CentralFreeList 找到合适的 span(non_empty), 看看 span 下是否有可用 object, 有就取几个到 ThreadCache 中用, 申请完毕.
- 没有就去 PageHeap 中要几页内存 (span), 拆成需要的 object, 放到 CentralCache 中, 再取几个给自己的 ThreadCache, 申请完毕.
- PageHeap 也没有, 就向系统要几页内存, 组织成 span.
申请大内存(>256K)
直接管 PageHeap 要一个刚好大于等于所需内存数量的 span 直接用, 不拆成 object 走上面那套管理.
释放小内存
- 直接放到 ThreadCache 里.
- 如果 freelist 长度超过阈值或 ThreadCache 容量超过阈值, 则触发回收.
- object 被回收到 CentralCache, 可能放到了 tc_slots_, 也可能放到 span, 取决于当时的状态.
- 如果回收导致 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 能获得一个相对合理的配额.

浙公网安备 33010602011771号