tcmalloc——为高并发场景而生的内存分配器

malloc

(Ps:全篇读完后再回头来看一下封面图)

0.前言

在我们写业务代码的过程中,业务代码与系统内核间的两层内存池往往容易被忽略,尤其是其中的C库内存池。

当代码申请内存时,首先会到达应用层内存池,如果应用层内存池有足够的可用内存,就会直接返回给业务代码,否则,它会向更底层的 C 库内存池申请内存。

PTmalloc, TCMalloc和JEMalloc都属于C库内存池。几乎所有程序都在使用 C 库内存池分配出的内存,比如java使用ptmalloc,golang使用tcmalloc等等。C 库内存池影响着系统下依赖它的所有进程。

对C库内存池的选择与优化虽然看似影响不大,但是随着并发量的增加,优化效果会愈发显著。因此C库内存池的选择常常是一种很好的无需改变代码的系统调优方式。

本文主要就golang借鉴的由google开发,大名鼎鼎的tcmalloc(thread-caching malloc)进行详解。

概括

1.传统malloc的设计

大致来说,就是将内存分成多个不同size的以下的object结构,串成双向链表,放入不同的size bin下。

tradition

freelist

2.传统malloc的缺点以及解决方法

缺点:

(1)锁竞争(最重要)

以上数据结构是非线程安全的,所以glibc进行内存操作时,会上锁,因此在多线程应用中,需要进行锁的等待,非常浪费时间。(glibc因历史原因,适用于单线程应用。)

(2)浪费空间

data链表需要加上4byte的header与4byte的footer分别指向前后数据,假设原本有一个对象需要6byte,则本来分配一个8byte给他即可,但是加上8byte的头尾之后,经过对齐处理,需要分配16byte。并且缺少有效的利用内部碎片的方式。

(3)缺少提速手段

缺少有效的调节措施在一定程度上对空间与时间进行协调。比如选择性的故意增大内存空间的消耗,换取速度的提升。

解决思路:

(1)各线程引入自己的缓冲层。

(2)引入更高级的数据类型包含objects,构建松散链表(此处指span而不是page)。原本的数据结构使用的是双向链表结构,每个元素都需要加头加尾浪费空间。使用松散链表之后可以在提高命中率的同时,节省空间的使用。只需要在更高级的数据结构上带上4byte的头与4byte的尾即可,不用在每个元素上带头带尾。

unrolled-linked-list

(3)可人为调整增加整个进程持有的内存数(引入Tcmalloc-Page概念)。

(这里有一个设定,一个tcmalloc-page只有所有的object都未被使用或者作为一个object的一部分一起被整个释放,才会被返还给back-end(pageheap),以待之后还给OS或者重新分割成许多另一种size-class的object用以分配。一个tcmalloc-page默认占用占用8KB内存。可以切割成16个512byte的小对象。而如果page的大小人为调整为32KB的话,则可以分割为64个512byte的小对象。64个小对象更不容易被全部同时释放,所以会有更多的内存搁置在middle-end中快速的供各线程使用,所以间接增加了整个进程的内存持有数)

3.tcmalloc的内存分割单位以及pageMap

一句话:一个span由多个page构成,用于分割成多个相同size的object供线程使用

情况1:当object <= page时,对span中的page进行再切分,切分成多个相同size的object

情况2:当object > page时,一个object由同一个span中多个page组成

(1)page:默认8KB

不是TLB中的那种page,是TCmalloc-Page,默认8KB(8KB=2^13B,当对page进行切割时,其中的每一个内存地址,只需要左移13位, 就可以找到其所属的pageId

unit

(2)span:由1个或多个连续的的page(1~128个)组成,默认8KB~1024KB

记录了起始page的pageId以及包含page数量,是一个链表结构,包含前后指针。(span中直接包含所含的page信息,可以找到page,而通过page则需要经过pageMap才能找到其归属的span)

一个span要么被拆分成多个相同size class的小对象用于小对象分配,要么作为一个整体用于中对象或大对象分配。当作用作小对象分配时,span的sizeclass成员变量记录了其对应的size

(3)object:由span切割成,有88种大小(size-class),8B~256KB

(4)PageMap:

给定一个page,如何确定这个page属于哪个span?

PageMap缓存了PageID到Span的对应关系

在这里插入图片描述

pageMap的优势:利用了radix-tree,即前缀树。如图不用一开始生成root中512个元素所指向的1024个数组,可以按需生成使用。相较于所有pageId存放在一个数组的情况极大的节省了内存空间。(当然真实情况是3层radix-tree,和图中所示有点区别)

4.tcmalloc(thread-caching malloc)介绍

internal
概括

3.1 front-end(线程安全)

分为Per-CPU模式以及Per-thread模式。

(1)Per-thread模式

front-end

在线程模式下,每一个线程都有自己的缓存(最小512KB)。

组成(1个freelist):

1.一个递增数组,放各种size-class(88种,8B~256KB)

2.每个数组元素下挂一串固定size的单向链表

使用:

在数组中,找到满足需求的size-class,查看是否有空object,有则使用,没有的话则找下一个size-class(数组全找完都没空的,就去middle-end拿一批过来)

(2)Per-CPU模式

per-cpu

在CPU模式下,给每一个逻辑CPU分配缓存。数据结构与线程模式相同。然而在每一个逻辑CPU中,可能出现各个线程来回抢占的情况,可能造成线程不安全的情况,因此使用了restartable sequences技术实现线程安全。

restartable sequences有点像一个函数,可以由一系列CPU指令构成。如果该线程被抢占,下次这一串指令得从头再来。此处省略详细介绍。

3.2 middle-end(非线程安全,需要spin-lock)

middle-end

组成(1个freeList+1个transfer-cache):

1.Transfer Cache(或者叫CentralCache) 中间层的缓存。 (transfer cache是一个数组,负责暂存指向暂时不用的objects的指针)

2.Free-List (即和front-end中一摸一样,一个递增size-class数组+下面挂的单向链表)

使用:

整个middle-end和front-end非常相似,可以看作是给所有线程再增加了一个全局共享的free-list。如果线程自己的free-list中的空闲object数量不够了,就来middle-end中的freelist来取。剩余得多也先还给middle-end。

在这里得注意middle-end的free-list是非线程安全的,所以需要上一个自旋锁(注意不是mutex),可能会影响性能,因此作者在middle-end中增加了一层transfer-cache,数据结构是一个数组,用于存放objects,线程归还多余的object或者拿object都先看一下transfer-cache中有没有。因此能做到线程之间快速交换objects。

3.3 back-end

back-end

组成(一个基本单位为page的free-list加一个span set):

1.一个存放不同page大小(1~128个page)的数组,每个数组指向一个含有对应page数量的span双向链表。

2.一个递增数组,存放含有>128 page的span

使用:

优先查找free-list,如果没有适合的,就查找large span set。再找不到的话就去向系统申请内存空间

5. tcmalloc与ptmalloc的对比

http://goog-perftools.sourceforge.net/doc/tcmalloc.html

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值