TCMalloc详解

  • TCMalloc
    • 虚拟内存
  • TCMalloc架构
    • TCMalloc Front-end
      • Per-thread模式
      • Front-end缓存运行时大小
      • 小对象和大对象分配
    • Middle-end
      • Transfer Cache
      • Central Free list
      • Pagemap 和 Spans
      • 在`spans`中存储小内存对象
    • TCMalloc Page Sizes
    • TCMalloc Back-end
      • Legacy Pageheap
    • TCMalloc详细架构图

本文部分内容是结合参考其他文章已经tcmalloc官方文档。

TCMalloc

​ TCMalloc 是 Google 对 C 的 malloc() 和 C++ 的 operator new 的自定义实现,用于在我们的 C 和 C++ 代码中进行内存分配。 TCMalloc 是一种快速、多线程的 malloc 实现。

​ TCMalloc为每个线程分配了缓存,这个缓存是线程私有的,可以减少多线程程序竞争。对于小对象的内存分配,首先会去请求线程缓存,不用加锁,如果缓存不能满足的话,需要去向后面的内存存储结构中获取,此时需要加锁获取,因为其他线程可能正在获取内存空间,但是大部分情况下线程缓存就可以满足内存请求,所以几乎不需要锁。对于大对象的内存分配,TCMalloc尝试着使用细粒度和高效的自旋锁。另外一个TCMalloc的好处是小对象内存分配效率高。例如,分配n个8 byte的对象时,使用大约8n * 1.01byte的空间,只有百分之一的空间浪费。ptmalloc2分配内存的方法为每个对象使用一个4 byte的标头,并且将大小四舍五入为8 byte的倍数,最终使用16n byte。

虚拟内存

​ 有一个结论需要提一下,我们的进程是运行在虚拟内存上的,图示如下:

  • 对于我们的进程而言,可使用的内存是连续的

  • 安全,防止了进程直接对物理内存的操作(如果进程可以直接操作物理内存,那么存在某个进程篡改其他进程数据的可能)

  • 虚拟内存和物理内存是通过MMU(Memory Manage Unit)映射的(感兴趣的可以研究下)

  • 等等(感兴趣的可以研究下)

    所以,以下文章我们所说的内存都是指虚拟内存

TCMalloc架构

​ 下面这幅图粗略的介绍了TCMalloc的内部结构:

​ 我们可以将TCMlloc分为三部分,front-end, middle-end, back-end。 它们的职责分别是:

  • Front-end是一个缓存,提供内存快速分配和重分配内存给应用程序的功能。它主要有2部分组成:Per-thread cache和Per-CPU cache。这里只聊 Per-thread模式。
  • Middle-end负责给front-end提供缓存。当front-end缓存不足时,首先从middle-end中获取。它由Central free list组成。
  • Back-end负责从系统获取内存。当middle-end中的内存不足时,从back-end中获取。它主要设计page heap的内容。

​ 注意一下,front-end可以在per-cpu或者legacy per-thread模式下运行,后端可以支持hugepage aware pageheap或者legacy pageheap。

TCMalloc Front-end

Front-end可以处理特定大小的内存分配请求。Front-end有一个线程缓存,可用于分配或保存空闲内存。这个缓存一次只能被一个线程访问,所以它不需要任何锁,因此大多数分配和释放都很快。

​ 如果Front-end具有适当大小的内存缓存,可以直接从缓存中为程序分配对象。如果该特定大小的内存块为空,则Front-end将从Middle-end请求一批内存以重新填充缓存。中端包括 CentralFreeListTransferCache

​ 如果Middle-end可用的内存空间耗尽,或者请求的大小大于Front-end缓存的最大大小,则请求会去Back-end中获取内存,或者重新填充Middle-end的内存。后端也称为 PageHeap。

​ TCMalloc 前端有两种实现:

  • 最初它支持对象的每线程缓存(因此得名Thread-local malloc)。然而,Thread-local 模式会导致内存占用随着线程数量的增加而增加。现代应用程序可能具有大量线程数,这会导致大量的线程占用了很多内存空间,或者许多线程具有很小的线程缓存。

    • 最近 TCMalloc 支持 per-CPU 模式。在这种模式下,系统中的每个逻辑 CPU 都有自己的缓存,可以从中分配内存。注意:在 x86 上,逻辑 CPU 相当于超线程。
      per-thread 和 per-CPU 模式之间的差异完全局限于 malloc/new 和 free/delete 的实现。

​当front-end处理小对象分配时,front-end会根据所需要内存大小,把它们向上取整到固定的60-80种可分配的大小,见此, 比如请求分配12kb时,会分配一块大小为16kb的内存,这样做的好处是所有分配出去的内存块大小都是固定的(虽然有不同的大小),可以避免内存碎片,当然,缺点就是有一小部分内存会被浪费。
其中有一个参数是 kMaxSize,如果所申请的内存大小比它大,那么就会直接从 back-end 去获取。

Per-thread模式

​ 在 per-thread 模式下,TCMalloc为每一个线程分配一个thread-local cache, 小内存的分配会直接从thread-local cache中获取。如果有需要也会把thread-local中的空闲的内从移到middle-end中。

​ 一个thead-local cache包含了一个空闲内存的单向链表,因为会给不同大小的内存分类,所以每个大小不同的内存都有各自的链表,如下图所示:

​ 分配小对象内存时,会在这个线程的tread-local cache中寻找合适大小的内存,在内存释放时,小内存块也会直接追加到该线程的tread-local cache中对应的链表中。如果thread-local cache中内存不足,那么就从middle-end中获取,如果内存太多,会把多余的内存放入middle-end中。

​ 当一个线程结束时,它的thread-local cache的内存会返回给middle-end

Front-end缓存运行时大小

​ 在 Front-end缓存中,对大小不同的内存块列表进行最佳调整很重要,如果列表太小,就需要从central free list(middle-end)中频繁获取内存, 要是列表太大,就会造成内存浪费。

​ 需要注意的是,缓存对于释放和分配同样重要,如果没有缓存,那么每次释放都需要将内存移动到中央空闲列表。

小对象和大对象分配

​ TCMalloc维护了一份空间大小映射表,当分配小对象内存空间时,会从这个表里寻找合适大小的内存,点这里能都看到。例如,12字节的分配将会寻找16字节大小的内存空间。空间大小级别是为了在向上取最小满足的内存空间时减少浪费。

​ 如果分配的内存空间大于1MB,那么直接从后端分配,因此,大对象内存空间不会缓存在front-endmiddle-end中。大对象分配时会向上取最小满足页大小的内存空间。

Middle-end

Middle-end负责为 Front-end提供内存以及把多余的内存放回Back-end. Middle-end是由Transfer cacheCentral free list组成。尽管Transfer cacheCentral free list经常被认为是一个东西,但它们是有区别的。当Front-end访问Middle-end时需要先加锁然后再获取内存,这会造成线性访问的时间消耗。

Transfer Cache

​ 当Front-end请求内存或者返回内存时,它们会先到Transfer cache中。

Transfer cache维护了一个指向空闲内存的指针数组,它可以快速地将对象移动到这个数组中,或者代表Front-end从这个数组中获取对象。

​ 对于Transfer cache,它可以为某个线程提供内存分配,可以接收被其它线程释放的内存,Transfer cache因此得名。Transfer cache允许内存在两个不同的线程之间快速流动。

​ 如果Transfer cache时不能满足内存分配的话,或者没有足够的空间去维护从 Front-end返回的内存,它将会访问Cental free list

Central Free list

Central free listspan的方式管理内存,一个span是一个或者多个内存TCMalloc页的集合。这个术语将会在下一部分解释。

Central free list通过从spans中提取内存来满足一个或者多个内存请求,直到这些内存请求全部被满足。如果在spans中没有足够的内存,则会从back-end中请求spans

​ 当内存返回至central free list 中,每块内存都会先去映射判断改内存大小属于那个span(使用pagemap做映射),然后放入合适的span中。如果放在一个特定span的内存全部返给cental free list中,这整个span会返回至back-end中。

Pagemap 和 Spans

​ 被TCMalloc管理的heap被划分至pages中,这些pages在编译期就决定了大小。一个连续的pagesspan对象表示,一个span可以用来管理一个交给应用程序的大内存空间对象,或者一个连续的页可以被分割到一系列的小内存对象。如果span管理小内存对象,那么这个内存对象的大小级别将会被记录在span中。

pagemap用来寻找属于某个大小的内存对象的span,或者用来鉴别某个内存对象的大小级别。

TCMalloc使用一个2级或者3级的基数树(radix tree),为了尽可能把所有的内存地址映射至spans中。

​ 下面的图展示了一个radix-2pagemap怎么样把映射内存对象的地址到spans中,这些 spans管理维护内存对象的pages。在这个图中span A管理两个pagesspan B管理三个 pages.

​ 在Middle-end中,spans用来决定放置被返回对象的地方,并且在back-end中用来管理pages范围的处理。

spans中存储小内存对象

​ 一个span包含了一个指向span控制的TCMalloc pages基址指针。对于小的内存对象,这些pages最多分为2的16次个对象,选择这个值是为了在span中我们可以通过一个2 byte的指针来引用对象。

​ 当我们没有某个大小级别的可用的内存对象时,我们需要从页堆中获取一个新的span并填充它。

TCMalloc Page Sizes

​ TCMalloc能够使用各种大小不同的页来构建。注意一下,这些与底层硬件的TLB中使用的页面大小不对应。这些TCMalloc页面大小目前为4KB, 8KB, 32KB和256KB。

​ TCMalloc页有两个使用方式,一个TCMalloc的页要么包含多个特定大小的对象,要么多个页组成一个group去存储一个大小超过单个页的对象。如果整个页是空闲的,它将会返回到back-end(page heap)中,过一段时间之后回重新用于保存不同大小的对象(或者返回到操作系统)。

​ 小pages能够在更少开销的情况下更好地处理应用程序的内存需求。例如,使用一半的 4KiB 页面将剩余 2KiB,而 32KiB 页面将有 16KiB。小pages也更有可能被回收。例如,一个 4KiB 的页面可以容纳 8 个 512 字节的对象,而 32KiB 的页面可以容纳 64 个512 字节的对象;有 64 个对象同时空闲的机会比 8 个对象同时空闲的可能性要小得多。

​ 大pages导致从后端获取和返回内存的频率变低。单个 32KiB page可以容纳 4KiB page对象可容纳的八倍,这会导致管理较大page的成本更小。使用较少的大pages就能映射整个虚拟地址空间。TCMalloc有一个 page 映射,它将虚拟地址映射到管理该地址范围内的对象的结构上。更大的pages意味着页面映射需要更少的条目,因此pagemap更小。

​ 因此,对于内存占用较小或对内存占用大小敏感的应用程序,使用较小的 TCMalloc page 大小是更合适的。具有大内存占用的应用程序可能会受益于更大的 TCMalloc page

TCMalloc Back-end

​ TCMalloc的Back-end有三个职责:

  • 管理大的不使用的内存。

  • 当没有合适大小的内存去满足内存分配请求时,向操作系统申请内存。

  • 负责返回不需要的内存至操作系统。

    TCMalloc有两个后端:

  • Legacy pageheap:管理在TCMalloc页大小块中的内存。

  • Hugepage aware pageheap:管理在大页块中的内存。在大页面块中管理内存使分配器能够通过减少 TLB 未命中来提高应用程序性能。

    这里只聊Legacy pageheap.

Legacy Pageheap

Legacy Pageheap是一个关于特定长度的可用内存连续页面的空闲列表数组。对于 k < 256,第 k 个条目是包含 k 个 TCMalloc 页的运行的空闲列表。 第 256 个条目是长度 >= 256 页的空闲运行列表:

​ 当为k个页分配内存时,会去查看第k个空闲列表, 如果该空闲列表为空,则查看下一个空闲列表,依此类推。 最后,如有必要,我们会查看最后一个空闲列表。 如果失败,我们从系统 mmap 中获取内存。

​ 如果长度 > k 的页面运行满足 k 个页面的分配,则将剩余的运行重新插入回页堆中适当的空闲列表中。

​ 当一系列页面返回到页堆时,将检查相邻页面以确定它们现在是否形成连续区域,如果是这种情况,则将页面连接起来并放入适当的空闲列表中。

TCMalloc详细架构图

​ 最后在来看一个完整的请求内存的流程。


​ 这里有一副详细TCMalloc架构图,其中有几个重要的概念:

  • page:操作系统对内存管理以页为单位,TCMalloc也是这样,只不过TCMalloc里的Page大小与操作系统里的大小并不一定相等,而是倍数关系。《TCMalloc解密》里称x64下Page大小是8KB。
  • Span:一组连续的Page被称为Span,比如可以有2个页大小的Span,也可以有16页大小的Span,Span比Page高一个层级,是为了方便管理一定大小的内存区域,Span是TCMalloc中内存管理的基本单位。
  • ThreadCache:每个线程各自的Cache,一个Cache包含多个空闲内存块链表,每个链表连接的都是内存块,同一个链表上内存块的大小是相同的,也可以说按内存块大小,给内存块分了个类,这样可以根据申请的内存大小,快速从合适的链表选择空闲内存块。由于每个线程有自己的ThreadCache,所以ThreadCache访问是无锁的。
  • CentralCache:是所有线程共享的缓存,也是保存的空闲内存块链表,链表的数量与ThreadCache中链表数量相同,当ThreadCache内存块不足时,可以从CentralCache取,当ThreadCache内存块多时,可以放回CentralCache。由于CentralCache是共享的,所以它的访问是要加锁的。
  • PageHeap:PageHeap是堆内存的抽象,PageHeap存的也是若干链表,链表保存的是Span,当CentralCache没有内存的时,会从PageHeap取,把1个Span拆成若干内存块,添加到对应大小的链表中,当CentralCache内存多的时候,会放回PageHeap。如下图,分别是1页Page的Span链表,2页Page的Span链表等,最后是large span set,这个是用来保存中大对象的。毫无疑问,PageHeap也是要加锁的。

    上文提到了小、中、大对象,Go内存管理中也有类似的概念,我们瞄一眼TCMalloc的定义:
  1. 小对象大小:0~256KB
  2. 中对象大小:257~1MB
  3. 大对象大小:>1MB

TCMalloc详解相关推荐

  1. Ubuntu16.04下Mongodb官网安装部署步骤(图文详解)(博主推荐)

    不多说,直接上干货! 在这篇博客里,我采用了非官网的安装步骤,来进行安装.走了弯路,同时,也是不建议.因为在大数据领域和实际生产里,还是要走正规的为好. Ubuntu16.04下Mongodb(离线安 ...

  2. Linux环境变量详解

    Linux环境变量详解 环境变量是操作系统环境设置的变量,适用于整个系统的用户进程. 环境变量分类 按照权限分类 系统级:系统级的环境变量是每个登录到系统的用户都要读取的系统变量 用户级:用户级的环境 ...

  3. 详解redis5.x版本

    详解redis5.x版本 关系型和非关系型 关系型数据库: mysql Oracle pg 非关系型数据库: redis MongoDB esnosql not only SQL nosql 指的是非 ...

  4. 从命令行到IDE,版本管理工具Git详解(远程仓库创建+命令行讲解+IDEA集成使用)

    首先,Git已经并不只是GitHub,而是所有基于Git的平台,只要在你的电脑上面下载了Git,你就可以通过Git去管理"基于Git的平台"上的代码,常用的平台有GitHub.Gi ...

  5. JVM年轻代,老年代,永久代详解​​​​​​​

    秉承不重复造轮子的原则,查看印象笔记分享连接↓↓↓↓ 传送门:JVM年轻代,老年代,永久代详解 速读摘要 最近被问到了这个问题,解释的不是很清晰,有一些概念略微模糊,在此进行整理和记录,分享给大家.在 ...

  6. docker常用命令详解

    docker常用命令详解 本文只记录docker命令在大部分情境下的使用,如果想了解每一个选项的细节,请参考官方文档,这里只作为自己以后的备忘记录下来. 根据自己的理解,总的来说分为以下几种: Doc ...

  7. 通俗易懂word2vec详解词嵌入-深度学习

    https://blog.csdn.net/just_so_so_fnc/article/details/103304995 skip-gram 原理没看完 https://blog.csdn.net ...

  8. 深度学习优化函数详解(5)-- Nesterov accelerated gradient (NAG) 优化算法

    深度学习优化函数详解系列目录 深度学习优化函数详解(0)– 线性回归问题 深度学习优化函数详解(1)– Gradient Descent 梯度下降法 深度学习优化函数详解(2)– SGD 随机梯度下降 ...

  9. CUDA之nvidia-smi命令详解---gpu

    nvidia-smi是用来查看GPU使用情况的.我常用这个命令判断哪几块GPU空闲,但是最近的GPU使用状态让我很困惑,于是把nvidia-smi命令显示的GPU使用表中各个内容的具体含义解释一下. ...

最新文章

  1. 你是个成熟的C位检测器了,应该可以自动找C位了
  2. ICCV 2021| GRF: 用于三维表征和渲染的通用神经辐射场(已开源)
  3. python比c语言好学吗-对于初学者而言,python和 c语言先学哪个好
  4. linux/CentOS 6忘记root密码解决办法
  5. 利用LFSR实现模2除法的原理
  6. python开发专属表情包_Python开发个人专属表情包网站
  7. 这里有一篇简单易懂的webSocket 快到碗里来~
  8. erlang精要(5)-列表推导式
  9. 无人驾驶方面牛人和实验室
  10. 【空间分析】0 基本空间分析工具
  11. 搭建SpringMVC详解
  12. MySQL(21)-----数据库事务
  13. EEG公开数据集汇总
  14. 各厂商服务器存储默认管理口登录信息(默认IP、用户名、密码)
  15. CAD导出pdf的正确方法(包括导出黑白pdf)
  16. 关于vs2015各版本的卸载
  17. SGD和带momentum的SGD算法
  18. 【Linux】rpm包是什么
  19. AM、PM是上午和下午的英文缩写、英文缩写(英语星期月份等)
  20. 计算机电源出现问题,电源故障引起的电脑问题

热门文章

  1. (一)Redis常用数据类型及应用场景(Redis的解决方案汇总)
  2. 三级计算机控制系统,机电系统计算机控制三级项目.doc
  3. DC 视频教程 第八期
  4. 如何调整html中音乐播放器的大小,HTML5音乐播放器(三):播放进度,时间显示以及音量的调节...
  5. 一台服务器能支持多少docker,一台物理机器部署多个docker
  6. uboot启动第二阶段
  7. libvirt入门并创建第一个虚拟机
  8. 虚拟机下安装linux mysql weblogic过程
  9. 系统分析与设计-homework1
  10. COleDateTime::ParseDateTime