高性能服务器为什么需要内存池?

news2024/11/25 0:35:32

C/C++程序为什么比起其它语言开发的程序效率要高,一个很重要的原因就是可以直接操作内存,今天就来讲讲为什么需要内存池的技术。

从一个示例开始

先看下面两段代码,都是去重复的创建和删除对象0x5FFFFF次,他们的执行后的效率怎么样呢?

程序一:

#include <iostream>
#include <time.h>

class CTestClass{
    char m_chBuf[4096];
};

int main(){
    clock_t count = clock();

    for(unsigned int i=0; i<0x5fffff; i++){
        CTestClass *p = new CTestClass;
        delete p;
    }

    std::cout << "Interval = " << clock() - count  << " ticks" << std::endl;

    return 0;
}

程序二:

#include <iostream>
#include <time.h>

char buf[4100];

class CTestClass{
    char m_chBuf[4096];
public:
    void *operator new(size_t size){
        return (void *)buf;
    }

    void  operator delete(void *p){}
};

int main(){
    clock_t count = clock();

    for(unsigned int i=0; i<0x5fffff; i++){
        CTestClass *p = new CTestClass;
        delete p;
    }

    std::cout << "Interval = " << clock() - count  << " ticks" << std::endl;

    return 0;
}

在我的机器上执行的情况是:

从上图的结果可以看出他们执行的效率大约相差8倍,那这是为什么呢?这是因为程序二重载了new运算后避免了使用malloc/free等系统调用,众所周知,系统调用是很耗时的操作。

内存是如何分配的

1. brk和mmap是什么鬼?

我们先看下系统内存的布局,如下图:

应用程序动态分配内存的区域就是用户栈和全局区直接的一块区域,这块区域允许我们使用mmap、munmap和brk去向内核申请和释放内存的系统调用,内核刚把应用程序加载时,brk指针指向全局区的高地址,当我们调用brk系统调用,内核把brk指针往高地址移动,同时内核把它所管理的这一块内存返还给用户,用户因此就获得一块可以自由操作的内存;mmap不一样的是直接从动态内存区域直接映射一块未被使用的内存块给用户,该内存块可以通过munmap释放。且看下面一段程序(程序三):

#include <unistd.h>
#include <iostream>
#include <time.h>

int main(){
    void* p = sbrk(0);
    clock_t count = clock();
    for(unsigned int i=0; i<0x5fffff; i++){
        int* p1 = (int*)p;
        int ret = brk(p1 + 256);  //malloc 1k
        p1[256] = 256;
        ret = brk(p);     // free 1k
    }

    std::cout <<"Interval = " << clock() - count << "ticks" << std::endl;

    return 0;
}

sbrk和brk都可以向内核申请内存和释放,只不过调用的方式不一样而已,上面通过sbrk获得应用程序刚加载的时候brk指针所在的位置,在for循环里通过调用brk函数去申请和释放内存,但是这种通过系统调用去申请和释放内存的效率怎么样?下图是测试的结果

​通过这种原始的系统调用去分配和释放内存的效率是相当低的,总体来讲存在以下三个问题:

  • brk、mmap和munmap都是系统调用,调用的成本是非常非常高的;

  • 释放的时候又直接归还给操作系统了,而实际上是还可以加以利用的;

  • brk最近申请的内存没有释放的话,之前申请的内存是无法释放的,比如brk申请1K,再申请2k,再申请4k内存,此时如果想释放最先申请的1K内存,那不好意思,brk不支持这个,因为brk函数智能移动brk指针,要等到4k和2k内存都释放了才能释放这个1k内存,内存碎片的产生的根源就是因为这。

相关视频推荐

200行代码实现slab,开启内存池的内存管理(准备linux环境)

5种内存泄漏检测方式,让你重新理解内存管理

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

免费直播地址:c/c++ linux服务器开发/后台架构师

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

2. glibc粉墨登场了

正是因为使用原始的系统调用brk、mmap和munmap存在很多问题,那么libc针对性的给出了优化的方案,如glibc一开始就调用brk或者mmap准备一块比较大的内存,然后把比较大的内存划分成小内存,当我们调用malloc时直接给用户,释放内存时并不一定就归还给内核,而是继续持有,以便调用malloc时能够继续用,而不用去向操作系统要(哦!我的措辞不够严谨,且听下文详细分析)。

glibc的内存块采用chunk管理,并且将大小相似的chunk用链表管理,一个链表被称为一个bin。chunk的结构如下图:

​总共是128个bin,前64个bin里,相邻的bin内的chunk大小相差8字节,称为small bin,后面的是large bin,large bin里的chunk按先大小,再最近使用的顺序排列。分配内存时从所有的bins里查找最合适的chunk。如果是特别大的内存则从top chunk分配,如果不够且小于128k则调用brk扩充top chunk的内存,否则直接调用mmap分配内存。

glibc的分配流程是:

  1. 获取分配区的锁,防止多线程冲突。

  2. 计算出需要分配的内存的chunk实际大小。

  3. 判断chunk的大小,如果小于max_fast(64b),则取fast bins上去查询是否有适合的chunk,如果有则分配结束。

  4. chunk大小是否小于512B,如果是,则从small bins上去查找chunk,如果有合适的,则分配结束。

  5. 继续从 unsorted bins上查找。如果unsorted bins上只有一个chunk并且大于待分配的chunk,则进行切割,并且剩余的chunk继续扔回unsorted bins;如果unsorted bins上有大小和待分配chunk相等的,则返回,并从unsorted bins删除;如果unsorted bins中的某一chunk大小 属于small bins的范围,则放入small bins的头部;如果unsorted bins中的某一chunk大小 属于large bins的范围,则找到合适的位置放入。

  6. 从large bins中查找,找到链表头后,反向遍历此链表,直到找到第一个大小 大于待分配的chunk,然后进行切割,如果有余下的,则放入unsorted bin中去,分配则结束。

  7. 如果搜索fast bins和bins都没有找到合适的chunk,那么就需要操作top chunk来进行分配了(top chunk相当于分配区的剩余内存空间)。判断top chunk大小是否满足所需chunk的大小,如果是,则从top chunk中分出一块来。

  8. 如果top chunk也不能满足需求,则需要扩大top chunk。主分区上,如果分配的内存小于分配阀值(默认128k),则直接使用brk()分配一块内存;如果分配的内存大于分配阀值,则需要mmap来分配;非主分区上,则直接使用mmap来分配一块内存。通过mmap分配的内存,就会放入mmap chunk上,mmap chunk上的内存会直接回收给操作系统。

具体如图:

​glibc的释放流程:

  1. 获取分配区的锁,保证线程安全。

  2. 如果free的是空指针,则返回,什么都不做。

  3. 判断当前chunk是否是mmap映射区域映射的内存,如果是,则直接munmap()释放这块内存。前面的已使用chunk的数据结构中,我们可以看到有M来标识是否是mmap映射的内存。

  4. 判断chunk是否与top chunk相邻,如果相邻,则直接和top chunk合并(和top chunk相邻相当于和分配区中的空闲内存块相邻)。转到步骤8

  5. 如果chunk的大小大于max_fast(64b),则放入unsorted bin,并且检查是否有合并,有合并情况并且和top chunk相邻,则转到步骤8;没有合并情况则free。

  6. 如果chunk的大小小于 max_fast(64b),则直接放入fast bin,fast bin并没有改变chunk的状态。没有合并情况,则free;有合并情况,转到步骤7

  7. 在fast bin,如果当前chunk的下一个chunk也是空闲的,则将这两个chunk合并,放入unsorted bin上面。合并后的大小如果大于64KB,会触发进行fast bins的合并操作,fast bins中的chunk将被遍历,并与相邻的空闲chunk进行合并,合并后的chunk会被放到unsorted bin中,fast bin会变为空。合并后的chunk和topchunk相邻,则会合并到topchunk中。转到步骤8

  8. 判断top chunk的大小是否大于mmap收缩阈值(默认为128KB),如果是的话,对于主分配区,则会试图归还top chunk中的一部分给操作系统。free结束。

那么glibc存在什么不足呢?

  • 后分配的内存先释放,因为 ptmalloc 收缩内存是从 top chunk 开始,如果与 top chunk 相邻的 chunk 不能释放, top chunk 以下的 chunk 都无法释放。

  • 多线程锁开销大, 需要避免多线程频繁分配释放。

  • 内存从thread的分配区中分配, 内存不能从一个分配区移动到另一个分配区, 就是说如果多线程使用内存不均衡,容易导致内存的浪费。比如说线程1使用了300M内存,完成任务后glibc没有释放给操作系统,线程2开始创建了一个新的arena,但是线程1的300M却不能用了。

  • 每个chunk至少8字节的开销很大

  • 不定期分配长生命周期的内存容易造成内存碎片,不利于回收。 64位系统最好分配32M以上内存,这是使用mmap的阈值。

3. 是时候请出tamalloc了

tcmalloc是Google开源的一个内存管理库, 作为glibc malloc的替代品。目前已经在chrome、safari等知名软件中运用。

Talk is cheap, show me the code。

#include <iostream>
#include <time.h>
#include <google/tcmalloc.h>

class CTestClass{    
    char m_chBuf[4096];
};

int main(){
    clock_t count = clock();    
    
    for(unsigned int i=0; i<0x5fffff; i++){
        CTestClass *p = new CTestClass;
        delete p;
    }
    
    std::cout << "Interval = " << clock() - count  << " sec" << std::endl;

    return 0;
}

同样的代码引入了tcmalloc分配内存后,效率怎么样呢?

​至此,我们做个总结:

​那tcmalloc又是如何做到的呢?

4.tcmalloc解决并发和内存碎片问题

我们先来看一张图:

我们先来解释一下,tcmalloc为每一个线程分配了一个ThreadCache区域,每个线程都从属于自己的ThreadCache里分配和释放内存,这样就避免了glibc里多线程分配和释放内存的时候的锁的操作,锁是很有代价的,从上文的对比测试我们也能查看出他们的差异,那么我们总结出今天的第一个设计上的结论:真正高效的并发不是使用互斥锁、原子操作和自旋锁去解决对共享资源的访问,而是不共享任何资源,如果真要共享那么实现线程/进程间的通信。

那么tcmalloc是如何优化内存碎片的问题呢?答案就是CentralCache,当释放内存的时候,ThreadCache依然遵循批量释放的策略,对象积累到一定程度就释放给 CentralCache;CentralCache发现一个 Span的内存完全释放了,就可以把这个 Span 归还给 PageHeap;PageHeap发现一批连续的Page都释放了,就可以归还给操作系统,当ThreadCache的内存不够用的时候,那么就向CentralCache去借内存,然后分配出去,那么我们就能得出一个结论内存可以通过CentralCache在ThreadCache间流动,那么就在glibc的再一次优化了内存利用率,降低了内存碎片率。

高性能服务器为什么需要内存池

最后我们以一位大师的话来总结为什么需要内存池的设计:

​为什么需要内存池

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

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

相关文章

DIP第7章知识点

文章目录 7.3 相关7.5 基图像7.6 傅里叶相关变换7.6.1 离散哈特利变换7.6.3 离散正弦变换 DIP的其他章节都好复习&#xff0c;唯独就这个第7章小波变换。复习起来十分头大&#xff0c;所以我开始写他的课后题&#xff0c;雾。 7.3 相关 已知两个连续函数 f ( x ) f(x) f(x) …

一篇博客教会你怎么使用Docker安装Redis

文章目录 1、搜索镜像2、拉取镜像3、下载配置文件4、修改配置文件5、启动容器 今天我们学习使用 Docker 安装 Redis。 1、搜索镜像 docker search redis2、拉取镜像 docker pull redis3、下载配置文件 wget http://download.redis.io/redis-stable/redis.conf下载的配置文件…

云原生Docker容器管理

docker容器相当于一个进程&#xff0c;性能接近于原生&#xff0c;几乎没有损耗&#xff1b; docker容器在单台主机上支持的数量成百上千&#xff1b; 容器与容器之间相互隔离&#xff1b; 镜像是创建容器的基础&#xff0c;可以理解镜像为一个压缩包 docker容器的管理 容器…

彩票中奖率的真相:用 JavaScript 看透彩票背后的随机算法

原本这篇文章是打算叫「假如我是彩票系统开发者」&#xff0c;但细想一下&#xff0c;如果在文章中引用太多的 JavaScript 的话&#xff0c;反而不是那么纯粹&#xff0c;毕竟也只是我的一厢情愿&#xff0c;彩票开发也不全如本文所讲&#xff0c;有所误导的话便也是得不偿失了…

理解 Java 关键字 final

原文链接 理解 Java 关键字 final final可以用来干什么 final是Java中非常常见的一个关键字&#xff0c;可以说每天都在使用它&#xff0c;虽然常见&#xff0c;但却也不见得都那么显而易见&#xff0c;今天就来研究一下final&#xff0c;以加深对它的理解和更合理的运用。 修…

gitstack使用教程

一、下载及安装 下载地址&#xff1a;https://gitstack.com/download/?spma2c4e.10696291.0.0.6d4c19a40qOauc 支持操作系统列表 本文基于2.3.12版本 下载完成后安装&#xff0c;默认安装路径为&#xff1a;c:\GitStack&#xff0c;安装过程中&#xff0c;gitstack服务会启…

医学图像分割 nnUNetV2 分割自定义2d数据集

文章目录 1 环境安装(Pytorch)1.1 安装conda1.1 安装pytorch1.3 安装nnUNet1.4 安装隐藏层&#xff08;可选&#xff09; 2 配置自定义数据集2.1 数据集格式2.2 创建需要目录2.3 数据格式转换2.3.1 修改路径与数据集名称2.3.2 修改训练集与测试集2.3.3 修改掩码所在的文件夹&am…

python基础----05-----函数的多返回值、函数的多种参数使用形式、函数作为参数传递、lambda匿名函数

一 函数的多返回值 if __name__ __main__:# 演示使用多个变量&#xff0c;接收多个返回值def test_return ():return 1,hello,Truex,y,z test_return()print(x)print(y)print(z)1helloTrue二 函数的多种参数使用形式 分为以下四种。 2.1 位置参数 位置参数调用函数时根据…

卡尔曼滤波与组合导航原理(五)序贯Kalman滤波

量测维数很高&#xff0c;而且能写成很多分量&#xff0c;每一个分量可以看成一个小量测&#xff0c;可以序贯进行量测更新 优点是&#xff1a;计算快&#xff0c;数字稳定性更好&#xff0c;我们知道矩阵求逆是和维数的三次方成正比&#xff0c;分成小矩阵求逆快&#xff08;都…

自学大语言模型之Bert和GPT的区别

Bert和GPT的区别 起源 2018 年&#xff0c;Google 首次推出 BERT&#xff08;Bidirectional Encoder Representations from Transformers&#xff09;。该模型是在大量文本语料库上结合无监督和监督学习进行训练的。 BERT 的目标是创建一种语言模型&#xff0c;可以理解句子中…

(Day1)配置云开发提供的模板

创建云开发项目 打开微信开发者工具&#xff1b;点击“项目”->“新建项目”&#xff1b;输入项目名称和选择项目所要保存的目录&#xff1b;输入自己的AppID&#xff1b; AppID的获取&#xff0c;需要登陆微信公众平台&#xff0c;并点击“开发管理”->"开发设置…

凸优化系列——凸函数

1.凸函数的定义 凸函数直观上来说&#xff0c;就是两点之间的函数值小于两点连线的函数值 常见凸函数 线性函数既是凸函数&#xff0c;也是凹函数 对于二次函数&#xff0c;如果Q矩阵是半正定矩阵&#xff0c;那么它的二阶导为Q为半正定矩阵&#xff0c;根据凸性判定的二阶条…

SpringCloud微服务架构 --- 高级篇

一、初识Sentinel 1.1、雪崩问题及解决方案 1.1.1、雪崩问题 微服务调用链路中的某个服务故障&#xff0c;引起整个链路中的所有微服务都不可用&#xff0c;这就是雪崩。 1.1.2、解决雪崩问题的常见方式有四种 1.1.2.1、超时处理 设定超时时间&#xff0c;请求超过一定时间…

Swagger原理

最近在基于Swagger进行二次开发&#xff0c; 来对项目的接口进行管理&#xff0c;功能实现了&#xff0c;但是不清楚swagger的工作原理&#xff0c;为了后续能更好利用Swagger来管理接口&#xff0c;而且能借鉴Swagger的原理&#xff0c;将项目中其他信息可视化展示&#xff0c…

什么是测试驱动开发?测试驱动开发有什么优点?

目录 前言 什么是TDD或测试驱动开发&#xff1f; 什么是软件单元测试&#xff1f; 什么是TDD&#xff1f; 测试驱动开发的好处 最后的想法 前言 测试是任何软件开发项目中最重要的步骤之一。如果跳过此过程&#xff0c;则结果可能是灾难性的-对项目和公司而言。但是什么时…

K8s in Action 阅读笔记——【11】Understanding Kubernetes internals

K8s in Action 阅读笔记——【11】Understanding Kubernetes internals 11.1 Understanding the architecture Kubernetes集群分为两个部分&#xff1a; k8s控制平面工作节点 控制平面的组件 构成控制平面的组件有&#xff1a; etcd&#xff1a;etcd是一个分布式的持久化键…

javascrip基础二十八:说说函数节流和防抖?有什么区别?如何实现?

一、是什么 本质上是优化高频率执行代码的一种手段 如&#xff1a;浏览器的 resize、scroll、keypress、mousemove 等事件在触发时&#xff0c;会不断地调用绑定在事件上的回调函数&#xff0c;极大地浪费资源&#xff0c;降低前端性能 为了优化体验&#xff0c;需要对这类事…

类与对象知识总结+构造函数和析构函数 C++程序设计与算法笔记总结(二) 北京大学 郭炜

类和对象 结构化程序设计 C语言使用结构化程序设计&#xff1a; 程序 数据结构 算法 程序由全局变量以及众多相互调用的函数组成。 算法以函数的形式实现&#xff0c;用于对数据结构进行操作。 结构化程序设计的不足&#xff1a; 结构化程序设计中&#xff0c;函数和其所…

《嵌入式系统》知识总结11:STM32串口

串行通信vs并行通信 • 并行&#xff1a;使用8根数据线一次传送一个字节&#xff08;或使用16根数据线一次传送2个字节&#xff0c;...&#xff09; • 串行&#xff1a;使用少量数据信号线&#xff08;8根以下&#xff09;&#xff0c;将数据逐位分时传送 • 并行vs串行&…

路径规划算法:基于秃鹰优化的路径规划算法- 附代码

路径规划算法&#xff1a;基于秃鹰优化的路径规划算法- 附代码 文章目录 路径规划算法&#xff1a;基于秃鹰优化的路径规划算法- 附代码1.算法原理1.1 环境设定1.2 约束条件1.3 适应度函数 2.算法结果3.MATLAB代码4.参考文献 摘要&#xff1a;本文主要介绍利用智能优化算法秃鹰…