内存优化-比glibc更快的tcmalloc

news2024/9/22 21:26:11

TCMalloc 是 Google 开发的内存分配器,在不少项目中都有使用,例如在 Golang 中就使用了类似的算法进行内存分配。它具有现代化内存分配器的基本特征:对抗内存碎片、在多核处理器能够 scale。据称,它的内存分配速度是 glibc2.3 中实现的 malloc的数倍。

之前在学习 Golang 内存管理的时候,发现 Golang 竟然就用了鼎鼎大名的 TCMalloc。我之前也喜欢写一些源码分析之类的文章,但渐渐发觉从源码出发虽然能够探究实现的细节,但这些东西更适合作为自己的学习笔记,如果要讲给别人,还是用一些更加可读的方式比较好。因此,这篇文章主要以看图说话为主,是为图解。

什么是TCmalloc

tcmalloc就是一个内存分配器,管理堆内存,主要影响malloc和free,用于降低频繁分配、释放内存造成的性能损耗,并且有效地控制内存碎片。glibc中的内存分配器是ptmalloc2,tcmalloc号称要比它快。一次malloc和free操作,ptmalloc需要300ns,而tcmalloc只要50ns。同时tcmalloc也优化了小对象的存储,需要更少的空间。tcmalloc特别对多线程做了优化,对于小对象的分配基本上是不存在锁竞争,而大对象使用了细粒度、高效的自旋锁(spinlock)。分配给线程的本地缓存,在长时间空闲的情况下会被回收,供其他线程使用,这样提高了在多线程情况下的内存利用率,不会浪费内存,而这一点ptmalloc2是做不到的。

tcmalloc区别的对待大、小对象。

tcmalloc将内存请求分为两类,大对象请求和小对象请求,大对象为>=32K的对象。

tcmalloc会为每个线程分配本地缓存,小对象请求可以直接从本地缓存获取,如果没有空闲内存,则从central heap中一次性获取一连串小对象。

tcmalloc对于小内存,按8的整数次倍分配,对于大内存,按4K的整数次倍分配。

当某个线程缓存中所有对象的总大小超过2MB的时候,会进行垃圾收集。垃圾收集阈值会自动根据线程数量的增加而减少,这样就不会因为程序有大量线程而过度浪费内存。

实际上tcmalloc为每个线程分配了一个线程局部的cache,线程需要的小对象都是在其cache中分配的,由于是thread local的,所以基本上是无锁操作(在cache不够,需要增加内存时,会加锁)。同时,tcmalloc维护了进程级别的cache,所有的大对象都在这个cache中分配,由于多个线程的大对象的分配都从这个cache进行,所以必须加锁访问。在实际的程序中,小对象分配的频率要远远高于大对象,通过这种方式(小对象无锁分配,大对象加锁分配)可以提升整体性能。

线程级别cache和进程级别cache实际上就是一个多级的空闲块列表(Free List)。一个Free List以大小为k bytes倍数的空闲块进行分配,包含n个链表,每个链表存放大小为nk bytes的空闲块。在tcmalloc中,<=32KB的对象被称作是小对象,>32KB的是大对象。在小对象中,<=1024bytes的对象以8n bytes分配,1025<size<=32KB的对象以128n bytes大小分配,比如:要分配20bytes则返回的空闲块大小是24bytes的,这样在<=1024的情况下最多浪费7bytes,>1025则浪费127bytes。而大对象是以页大小4KB进行对齐的,最多会浪费4KB - 1 bytes。

如何分配定长记录

首先是基本问题,如何分配定长记录?例如,我们有一个 Page 的内存,大小为 4KB,现在要以 N 字节为单位进行分配。为了简化问题,就以 16 字节为单位进行分配。

解法有很多,比如,bitmap。4KB / 16 / 8 = 32, 用 32 字节做 bitmap即可,实现也相当简单。

出于最大化内存利用率的目的,我们使用另一种经典的方式,freelist。将 4KB 的内存划分为 16 字节的单元,每个单元的前8个字节作为节点指针,指向下一个单元。初始化的时候把所有指针指向下一个单元;分配时,从链表头分配一个对象出去;释放时,插入到链表。

由于链表指针直接分配在待分配内存中,因此不需要额外的内存开销,而且分配速度也是相当快。

相关视频推荐

90分钟了解Linux内存架构,numa的优势,slab的实现,vmalloc的原理

【C++开发】庞杂的内存问题,如何理出自己的思路出来,让你开发与面试双丰收

免费学习地址:C/C++Linux服务器开发/后台架构师

需要C/C++ Linux服务器架构师学习资料加qun579733396获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

如何分配变长记录

定长记录的问题很简单,但如何分配变长记录的。对此,我们把问题化归成对多种定长记录的分配问题。

 我们把所有的变长记录进行“取整”,例如分配7字节,就分配8字节,31字节分配32字节,得到多种规格的定长记录。这里带来了内部内存碎片的问题,即分配出去的空间不会被完全利用,有一定浪费。为了减少内部碎片,分配规则按照 8, 16, 32, 48, 64, 80这样子来。注意到,这里并不是简单地使用2的幂级数,因为按照2的幂级数,内存碎片会相当严重,分配65字节,实际会分配128字节,接近50%的内存碎片。而按照这里的分配规格,只会分配80字节,一定程度上减轻了问题。

大对象如何分配

上面讲的是基于 Page,分配小于Page的对象,但是如果分配的对象大于一个 Page,我们就需要用多个 Page 来分配了:

 这里提出了 Span 的概念,也就是多个连续的 Page 会组成一个 Span,在 Span 中记录起始 Page 的编号,以及 Page 数量。

分配对象时,大的对象直接分配 Span,小的对象从 Span 中分配。

Span如何分配

对于 Span的管理,我们可以如法炮制:

 还是用多种定长 Page 来实现变长 Page 的分配,初始时只有 128 Page 的 Span,如果要分配 1 个 Page 的 Span,就把这个 Span 分裂成两个,1 + 127,把127再记录下来。对于 Span 的回收,需要考虑Span的合并问题,否则在分配回收多次之后,就只剩下很小的 Span 了,也就是带来了外部碎片 问题。

为此,释放 Span 时,需要将前后的空闲 Span 进行合并,当然,前提是它们的 Page 要连续。

问题来了,如何知道前后的 Span 在哪里?

从Page到Span

由于 Span 中记录了起始 Page,也就是知道了从 Span 到 Page 的映射,那么我们只要知道从 Page 到 Span 的映射,就可以知道前后的Span 是什么了。

 最简单的一种方式,用一个数组记录每个Page所属的 Span,而数组索引就是 Page ID。这种方式虽然简洁明了,但是在 Page 比较少的时候会有很大的空间浪费。

为此,我们可以使用 RadixTree 这种数据结构,用较少的空间开销,和不错的速度来完成这件事:

 

乍一看可能有点懵,这个跟 RadixTree 能扯上关系吗?可以把 RadixTree 理解成压缩过的前缀树(trie),所谓压缩,就是在一条路径上的节点都只有一个子节点,就把这条路径合并到父节点去,因此内部节点最少会有 Radix 个字节点。具体的分析可以参考一下 wikipedia 。

实现时,可以通过一定的空间换来时间,也就是减少层数,比如说3层。每层都是一个数组,用一个地址的前 1/3 的bit 索引数组,剩下的 bit 对下一层进行寻址。实际的寻址也可以非常快。

PageHeap

到这里,我们已经实现了 PageHeap,对所有 Page进行管理:

 

全局对象如何分配

既然有了基于 Page 的对象分配,和Page本身的管理,我们把它们串起来就可以得到一个简单的内存分配器了:

 按照我们之前设计的,每种规格的对象,都从不同的 Span 进行分配;每种规则的对象都有一个独立的内存分配单元:CentralCache。在一个CentralCache 内存,我们用链表把所有 Span 组织起来,每次需要分配时就找一个 Span 从中分配一个 Object;当没有空闲的 Span 时,就从 PageHeap 申请 Span。

看起来基本满足功能,但是这里有一个严重的问题,在多线程的场景下,所有线程都从CentralCache 分配的话,竞争可能相当激烈。

ThreadCache如何分配

到这里 ThreadCache 便呼之欲出了:

 每个线程都一个线程局部的 ThreadCache,按照不同的规格,维护了对象的链表;如果ThreadCache 的对象不够了,就从 CentralCache 进行批量分配;如果 CentralCache 依然没有,就从PageHeap申请Span;如果 PageHeap没有合适的 Page,就只能从操作系统申请了。

在释放内存的时候,ThreadCache依然遵循批量释放的策略,对象积累到一定程度就释放给 CentralCache;CentralCache发现一个 Span的内存完全释放了,就可以把这个 Span 归还给 PageHeap;PageHeap发现一批连续的Page都释放了,就可以归还给操作系统。

至此,TCMalloc 的大体结构便呈现在我们眼前了。

Tcmalloc总结

这里用图解的方式简单讲述了 TCMalloc 的基本结构,如何减少内部碎片,如何减少外部碎片,如何使用伙伴算法进行内存合并,如何使用单链表进行内存分配,如何通过线程局部的方式提高扩展性。

不过实现一个高性能的内存分配器绝非如此简单,TCMalloc 中有许多策略,许多参数,许多细节的考量,都值得我们深究。一篇文章难以覆盖,之后的文章再做详解: 如何在项目中使用tcmalloc, 如何使用tcmalloc查内存泄漏

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/465140.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

3d网上渲染平台是怎么渲图的_云渲染流程详解!

题主说的看到许多网友对‘’3d网上渲染平台是怎么渲图的‘’进行提问&#xff0c;瑞云渲染小编也提供自己的小小见解。针对3D网上渲染平台是指什么&#xff0c;实际应该是指云渲染农场。几十年来&#xff0c;随着计算机软硬件不断更迭&#xff0c;图形图像渲染的效果更加清晰、…

信号完整性分析基础知识之传输线和反射(二):阻性负载的反射,源端阻抗,弹跳图

传输线的端接需要考虑三种重要的特殊情况&#xff0c;每种情况中&#xff0c;传输线的特性阻抗均为50Ohm。信号将从源端在这条传输线上传播&#xff0c;并以特定的阻抗端接到达远端。 TIP:在时域中&#xff0c;信号对瞬时阻抗十分敏感&#xff0c;第二区域并不一定是一条传输线…

常见的链表的OJ题

在本次的博客当中&#xff0c;为了巩固关于链表技能的运用&#xff0c;我们先来看一些与链表有关的OJ题。 &#x1f335;反转链表 题目详情如下&#xff1a; 第一道题目从逻辑上看不难&#xff0c;我们只需要将链表进行拆分&#xff0c;将我们下一个节点进行一个类似于头插的操…

【Java 数据结构】Map和Set

&#x1f389;&#x1f389;&#x1f389;点进来你就是我的人了 博主主页&#xff1a;&#x1f648;&#x1f648;&#x1f648;戳一戳,欢迎大佬指点!人生格言&#xff1a;当你的才华撑不起你的野心的时候,你就应该静下心来学习! 欢迎志同道合的朋友一起加油喔&#x1f9be;&am…

35岁程序员被裁赔偿27万,公司又涨薪让我回去,前提是退还补偿金,能回吗?

在大多数人眼里&#xff0c;35岁似乎都是一道槛&#xff0c;互联网界一直都有着“程序员是吃青春饭”的说法&#xff0c;。 如果在35岁的时候被裁能获得27万的赔偿&#xff0c;公司又涨薪请你回去上班&#xff0c;你会怎么选&#xff1f; 最近&#xff0c;就有一位朋友在网上…

Linux安装miniconda3

下载Miniconda&#xff08;Python3版本&#xff09; 下载地址&#xff1a;https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh 安装Miniconda&#xff08;需要连网&#xff09; &#xff08;1&#xff09;将Miniconda3-latest-Linux-x86_64.sh上传到/o…

研读Rust圣经解析——Rust learn-14(面向对象)

研读Rust圣经解析——Rust learn-14&#xff08;面向对象&#xff09; Rust面向对象对象包含数据和行为封装继承多态 实现面向对象书写最外层逻辑userServiceUser Rust面向对象 在一些定义下&#xff0c;Rust 是面向对象的&#xff1b;在其他定义下&#xff0c;Rust 不是 对象…

算法刷题|300.最长递增子序列、674.最长连续递增序列、718.最长重复子数组

最大递增子序列 题目&#xff1a;给你一个整数数组 nums &#xff0c;找到其中最长严格递增子序列的长度。 子序列 是由数组派生而来的序列&#xff0c;删除&#xff08;或不删除&#xff09;数组中的元素而不改变其余元素的顺序。例如&#xff0c;[3,6,2,7] 是数组 [0,3,1,6…

c++文件操作Ofstream、Ifstream,如何获取文件长度

一、文件光标定位streampos 在读写文件时&#xff0c;有时希望直接跳到文件中的某处开始读写&#xff0c;这就需要先将文件的读写指针指向该处&#xff0c;然后再进行读写。 ifstream 类和 fstream 类有 seekg 成员函数&#xff0c;可以设置文件读指针的位置&#xff1b;ofstr…

OpenGL光照:颜色

知识点归纳 现实世界中有无数种颜色&#xff0c;每一个物体都有它们自己的颜色。我们要做的工作是使用(有限的)数字来模拟真实世界中(无限)的颜色&#xff0c;因此并不是所有的现实世界中的颜色都可以用数字来表示。然而我们依然可以用数字来代表许多种颜色&#xff0c;并且你甚…

autosar

一 autosar简介 AUTOSAR&#xff0c;汽车开放系统架构&#xff08;AUTomotive Open System Architecture&#xff09;是一家致力于制定汽车电子软件标准的联盟。AUTOSAR是由全球汽车制造商、部件供应商及其他电子、半导体和软件系统公司联合建立&#xff0c;各成员保持开发合作…

QT DAY2

#include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this);this->setFixedSize(600,600); //设置固定尺寸this->setWindowTitle("汪玉洁大聪明")…

Hadoop学习笔记(一)Hadoop的组成

1. HDFS NameNode用于记录整个数据的存储情况&#xff0c;具体的数据存储在各个Hadoop节点中&#xff0c;每个Hadoop的节点可以称为DataNode。假设Hadoop1到Hadoop100的机器每个都有1T的容量。那么一共就可以存储100T的数据。 NameNode(nn)&#xff1a;存储文件的元数据&…

位运算【巧妙思路、两种常见题型】

这里介绍两种代码中位运算非常常用的操作 n的二进制表示中第k位数——右移操作 &1 例如说&#xff0c;我们需要计算11的第2位数。 11 (1011)2 我们常规思路就是将其转化为二进制数后&#xff0c;直接观察对应位置的值 这里需要注意的是第k位数指的是从右开始的第k位&a…

Linux shell编程 条件语句

条件测试 test命令 测试表达式是否成立&#xff0c;若成立返回0&#xff0c;否则返回其他数值 格式1: test 条件表达式 格式2: [ 条件表达式 ]文件测试 [ 操作符 文件或者目录 ][ -e 1.txt ]#查看1.txt是否存在&#xff0c;存在返回0 echo $? #查看是上一步命令执行结果 0成…

DJ4-3 连续分配存储管理方式

目录 4.3.1 单一连续分配 4.3.2 固定分区分配 1. 分区说明表 2. 内存分配过程 4.3.3 动态分区分配 一、分区分配中数据结构 二、分区分配算法 三、分区分配操作 4.3.4 可重定位分区分配 1. 紧凑 2. 动态重定位 3. 动态重定位分区分配算法 连续分配是指为用户程…

【数据结构】堆(一)

&#x1f61b;作者&#xff1a;日出等日落 &#x1f4d8; 专栏&#xff1a;数据结构 如果我每天都找出所犯错误和坏习惯&#xff0c;那么我身上最糟糕的缺点就会慢慢减少。这种自省后的睡眠将是多么惬意啊。 目录 &#x1f384;堆的概念及结构&#xff1a; &#x1f384;堆的实…

万丈高楼平地起 AI帮你做自己

AI的自我介绍 AI是人工智能&#xff08;Artificial Intelligence&#xff09;的英文缩写&#xff0c;是一种通过计算机技术模拟和延伸人类智能的技术和应用。AI可以被看作是一种智能化的计算机程序或系统&#xff0c;它能够自动地执行一些需要人类智能才能完成的任务&#xf…

JavaEE初阶学习:初识网络

1.网络发展史 1.独立模式 独立模式:计算机之间相互独立&#xff1b; 2.网络互连 随着时代的发展&#xff0c;越来越需要计算机之间互相通信&#xff0c;共享软件和数据&#xff0c;即以多个计算机协同工作来完成业务&#xff0c;就有了网络互连。 网络互连&#xff1a;将多…

除了Figma,再给你介绍10款好用的协同设计软件

组织结构越来越复杂&#xff0c;团队中的每个人都有独特的技能、经验和专业知识。我们怎样才能让团队更好地合作&#xff1f;在这种情况下&#xff0c;协同设计应运而生。 UI的未来是协同设计&#xff01;如果你想把握未来的设计趋势&#xff0c;不妨从使用高效的协同设计软件…