浅谈STL中的分配器

news2024/11/15 10:19:51

分配器是STL中的六大部件之一,是各大容器能正常运作的关键,但是对于用户而言确是透明的,它似乎更像是一个幕后英雄,永远也不会走到舞台上来,观众几乎看不到它的身影,但是它又如此的重要。作为用户,你几乎不用关心它的底层是怎么实现的,甚至也很少有能使用到它的机会。这里简单聊一下我对它的认识。

正常情况下我们如何取得一块内存?

  • malloc能够帮你获取一块内存并返回这块内存的首地址;
  • new operator的底层也是用malloc实现,只是相较于malloc,它不光会给你一块内存,还会帮你自动初始化这块内存,即调用对应对象的构造函数
  • operator new是C++获取内存的方式,注意:new operator和operator new是两种不同的东西,它也是调用了malloc来实现获取内存,只是封装了一些东西,增加了一些异常机制。
  • 而VC,BC,GNU C等等编译器厂商最初提供的allocate的底层也是通过调用operator new实现的。

所以,你发现没有?殊途同归,大家几乎都是通过调用malloc来实现获取内存这一操作的。而malloc根据机器的不同,去调用操作系统底层提供的api接口去获得真正的内存。

在这里插入图片描述

但是,如果你申请一块10个字节的内存,malloc给你的内存的大小却并不真的是10个字节。这里面你能用的内存有10个字节没错,但是还会有一些额外的开销在里面,它们会在这块内存的两头加上所谓的“cookie”来处理一些其他事情,就比如你买东西收到的其实并不是东西本身,还会有快递盒,快递袋,快递单等额外的东西帮助你自己买的东西到达你的手上。这些东西对你来说可能没用,但确确实实是不可避免的开销。

从这个角度而言,如果一个容器里放的东西很小,但是元素的数量又很多,假如容器里你想放一个2个字节的short类型的元素,而这样的容器的数量有100w个,这样轮到这个容器底层的分配器去帮你开辟内存的时候,由于cookie的存在,申请一个这样的容器你可能会得到10个字节,其中2个字节是你想要的内存,其余8个字节是额外的开销,这样下来100w个容器本来只需要200w个字节,现在你却不得不得到1000w个字节,性能实在是不这么高。

这里并不是说cookie很消耗内存才造成的你的性能不理想,而是存在一个比例问题。如果你的容器里放的元素的内存很大,那么这额外的开销就显得很渺小,完全可以接受;但是更多的情况下,容器里放的元素其实并没有那么大,这也就显得性能不理想。

如何解决这种问题?

SGI STL中给出的一个思路是先放很多的分配器,但是每个分配器只负责某种固定大小的内存的申请,等到容器真的申请内存的时候,对应大小的分配器会去申请一块很大的内存,然而自己将这些内存切割成固定大小的内存,再返回给使用者某一块固定大小的内存的首地址。

使用这种策略,便不再会对额外开销产生困扰,因为真正的申请内存只有刚开始的那次,所以只会得到一次cookie,得到的这块大的内存被切割成固定大小时,每块内存上并不会带cookie,也就不会有额外开销。
在这里插入图片描述

STL提供了两层内存分配器

  • 当分配大于128KB时,直接采用new operator,也就是一级内存分配器;
  • 当分配小于128KB时,采用二级内存分配器,也就是内存池,具体是通过自由链表实现的。参考文章。

为什么要分两级呢?主要是为了减少内存碎片,减少malloc的次数。所以内存池就相当于应用代码和系统调用申请内存的中间件。

第一层内存分配器

operator new

operator new可以被重载:

  1. 重载时,返回类型必须声明为void*;
  2. 重载时,第一个参数类型必须为分配空间的大小(字节),类型为size_t,当然也可以带其它参数;

如:

   class Foo
   {
   public:
     static void *operator new (size_t size)
     {
         Foo *p = (Foo*)malloc(size);
         return p;
     }
   
    static void operator delete(void *p, size_t size)
     {
         free(p);
     }
   };

这里只是简单的用mallocfree来实现,后续可以用内存池。

C++还提供了全局的operator newoperator delete,可以通过::operator new::operator delete来访问全局操作符。

placement new

operator new实现了new表达式的第一步即分配内存,那么谁来调用构造函数呢?就是placement new,它的语法是:

 Object * p = new (address) ClassConstruct(...)

这里要求addressvoid*,并且placement new被定义在#include<new>头文件中。同样的也可以重载它,也提供了全局下的placement new,通过::访问。

举个例子

 int* ptr = ::operator new(sizeof(int));
 ::new ((void*)ptr) int(); 

其实本质上placement new也是operator new的一个重载版本!只不过,这个重载版本我们常用来调用构造函数。如:

 class Foo
 {
 public:
     //一般的 operator new 重载
     void* operator new(size_t size)
     { return malloc(size); }
 
     //标准库已经提供的 placement new() 的重载形式
     void* operator new(size_t size, void* start)
     { 
       dosomething;
       return start; 
     }
 };    

那对new operatordelete operator拆分为两部分功能有什么好处呢?使用new表达式在分配内存时,需要在堆中查找足够大的剩余空间,显然这个操作速度是很慢的,而且有可能出现无法分配内存的异常(空间不够)。

placement new就可以解决这个问题。在一个预先准备好了的内存缓冲区上进行构造函数,不需要查找内存,内存分配的时间是常数。而且不会出现在程序运行中途出现内存不足的异常。所以,placement new非常适合那些对时间要求比较高,长时间运行不希望被打断的应用程序。

总之,new造成的反复分配内存很浪费,所以placement new直接固定内存,在这个固定内存上反复构造和析构,但不再反复分配内存和释放内存。

note:如果采用placement new,可别忘记在operator delete前调用析构函数!除非元素的析构函数是无关紧要的。

allocator

STL的allocator负责对容器的分配内存、释放内存、调用元素的构造函数、调用元素的析构函数。

其实理解了上面的内容,STL的allocator也就很简单。

对外提供四大方法:

  • allocator方法:即调用operator new
  • construct方法:即调用placement new
  • deallocator方法:即调用operator delete
  • destroy方法:即调用~T()

note:不是所有类都需要调用destroy,当类的析构函数是无关紧要的时候,我们可以不进行析构,那么什么样的是无关紧要的?可以用std::is_trivially_destructible类模板判断。具体来说:

  1. 使用隐式定义的析构函数,即没有定义自己析构函数
  2. 析构函数不是虚函数
  3. 其基类与非静态成员也是可trivially析构

其实会发现basic_string在释放内存前没有调用析构函数,正是因为basic_string严格要求元素类的析构函数是无关紧要的。而vector等则需要在释放内存前调用析构函数。

第二层内存分配器

先申请一大块内存,然后切割成小块,由单向链表串起来,内存池包括十六条链表,分别负责不同大小的内存大小,比如第7个负责256字节的区块,以8的倍速增长。

至于STL内存池设计的好坏也颇有争议:C++ 标准库中的allocator是多余的,allocator作为模板参数这就导致不同allocator是不同的type。

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

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

相关文章

Vue组件的几种通信方式

这里写目录标题 Vue组件的几种通信&#xff08;数据传递&#xff09;方式非父子组件间通信&#xff08;Bus事件总线&#xff09;介绍实例 非父子通信-provide&inject1.作用2.场景3.语法4.注意 父子组件间的通信固定props属性名&#xff08;v-model&#xff09;介绍实例 不固…

【部署运维】docker:入门到进阶

0 前言 部署运维博客系列一共有三篇&#xff1a; 拥抱开源&#xff0c;将工作中的经验分享出来&#xff0c;尽量避免新手踩坑。 【部署运维】docker&#xff1a;入门到进阶 【部署运维】kubernetes&#xff1a;容器集群管理掌握这些就够了 【部署运维】pythonredisceleryd…

C++多态-虚函数

多态分为编译时多态和运行时多态。编译时多态就是在编译阶段就能绑定要执行的那个函 数。运行时多态要等到运行到调用的那条语句时&#xff0c;根据指针/引用所绑定的对象&#xff0c;来决定执行哪 个函数&#xff0c;我们要讲的虚函数就是运行时多态&#xff0c;是 C中非常重…

嵌入式的学习需要合理规划时间

低级的欲望放纵即可获得&#xff0c;高级的欲望只有克制才能达成。——卡耐基1、粉丝的误会 很多粉丝&#xff0c;问我&#xff0c; "胡老师我想报您的培训班。" ... 得知我知识业余时间写文章&#xff0c;紧接着又会问&#xff0c; "jg单位这么清闲啊&#…

带你理解H桥电路

H桥电路 文章目录 H桥电路前言一、H桥基本结构二、H桥原理二、控制模式三、MOS管H桥 前言 在做单片机控制小车的时候一定会用一个电机驱动板&#xff0c;这个电机驱动板是怎么做的呢&#xff0c;答案就是H桥&#xff0c;没学过电路的同学可能会问什么是H桥&#xff0c;这篇文章…

【c++哈夫曼树代码实现】

哈夫曼树是不定长编码方式&#xff0c;由于是将权值大的元素放在离根结点近的地方 &#xff0c;权值小的放在离根远的地方&#xff0c;哈夫曼树效率很高&#xff0c;并且一个编码不会以另一个编码作为前缀&#xff0c;避免了编码的歧义性&#xff0c;本文将带大家探索如何创建和…

SAP smartform 实现打印条形码

先在SE73里定义一个新的BARCODE&#xff0c;注意一定要用新的才可以&#xff0c;旧的是打印不出来的。 然后定义一个SMARTFORM的样式&#xff0c;把你定义的BARCODE放到字符样式里面去。 再做SMARTFORM就可以了&#xff0c;将需要作为条码的变量的格式选为该BARCODE格式&…

ASP.NET Core 使用 SignalR 实现实时通讯

&#x1f433;简介 SignalR是一个用于ASP.NET的库&#xff0c;它允许服务器代码向连接的客户端实时发送推送通知。它使用WebSockets作为底层传输机制&#xff0c;但如果浏览器不支持WebSockets&#xff0c;它会自动回退到其他兼容的技术&#xff0c;如服务器发送事件&#xff…

GEE:众数滤波

作者:CSDN @ _养乐多_ 在本文中,我们将介绍如何使用Google Earth Engine(GEE)平台对遥感影像进行众数滤波处理。并以众数滤波平滑NDVI图像为示例,演示众数滤波整个过程。 结果如下图所示, 文章目录 一、众数滤波二、完整代码三、代码链接一、众数滤波 众数滤波是一种图…

保护您的IP地址:预防IP地址盗用的关键措施

随着互联网的发展&#xff0c;IP地址作为标识互联网设备的重要元素&#xff0c;成为网络通信的基石。然而&#xff0c;IP地址盗用威胁正不断增加&#xff0c;可能导致敏感信息泄露、未经授权的访问和网络攻击。本文将介绍一些有效的方法&#xff0c;以帮助组织和个人预防IP地址…

【外贸干货】领英客户开发与营销的六个策略方向

领英(LinkedIn)已经成为外贸营销人员&#xff0c;尤其是B2B外贸营销人员&#xff0c;一个重要且有效的社交媒体平台。 相比于其他社交媒体平台&#xff0c;领英(LinkedIn)在增加流量、产生高质量的潜在客户和建立思想领导力方面有着独有的优势。 因为领英(LinkedIn)不仅仅是获…

【力扣:1707 1803】0-1字典树

思路&#xff1a;树上每个节点存储拥有该节点的数组元素的最小值&#xff0c;left节点表示0&#xff0c;right节点表示1&#xff0c;构建完成后遍历树当子节点没有比mi小的元素时直接输出-1&#xff0c;否则向下构造。 struct tree{int m;tree*leftnullptr,*rightnullptr;tree…

flink源码分析之功能组件(二)-kubeclient

简介 本系列是flink源码分析的第二个系列,上一个《flink源码分析之集群与资源》分析集群与资源,本系列分析功能组件,kubeclient,rpc,心跳,高可用,slotpool,rest,metrics,future。其中kubeclient上一个系列介绍过,为了系列完整性,这里“copy”一下。 kubeclient组件…

ffmpeg下载与配置环境变量

FFmpeg 是一个强大的多媒体框架&#xff0c;可以让用户处理和操纵音频和视频文件。具有易于使用的界面&#xff0c;用户可以在 Windows、Mac 或 Linux Ubuntu 系统上下载 FFmpeg 并将其提取到文件夹中。然后&#xff0c;该软件可以加入 PATH 环境变量中就可以快捷的使用软件了.…

中职组网络安全-Windows操作系统渗透测试 -20221219win(环境+解析)

B-4:Windows操作系统渗透测试 任务环境说明: 服务器场景:20221219win 服务器场景操作系统:Windows(版本不详)(封闭靶机) 1.通过本地PC中渗透测试平台Kali对服务器场景Server08进行系统服务及版本扫描渗透测试,并将该操作显示结果中1433端口对应的服务版本信息作为F…

深度学习之图像分类(十四)CAT: Cross Attention in Vision Transformer详解

IPSA和CPSA的处理流程、维度变换细节 FLOPs的计算方法、以及flops和划分的patch数目以及patch的维度计算关系 IPSA如何进行local attention、CPSA如何进行globe attention CAT的代码详细注释---需要学习完Transformer TNT、swin transformer、crossViT CAT: Cross Atten…

软件测试 | MySQL 非空约束详解

&#x1f4e2;专注于分享软件测试干货内容&#xff0c;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01;&#x1f4e2;交流讨论&#xff1a;欢迎加入我们一起学习&#xff01;&#x1f4e2;资源分享&#xff1a;耗时200小时精选的「软件测试」资…

JavaWeb学习(未完结)

文章目录 一、基本概念1.1 动态Web网站简介1.2 web应用程序1.3 静态web1.4 动态web 二、web服务器2.1 技术2.2 应用服务器2.3 安装 jdk8 三、Tomcat3.1 安装 Tomcat93.2 文件说明3.3 启动并使用Tomcat3.4 关闭Tomcat3.5 可能遇到的问题3.6 配置3.6.1 修改测试访问的网页地址3.6…

LLM之Agent(一):使用GPT-4开启AutoGPT Agent自动化任务完整指南

在ChatGPT引领的大模型时代&#xff0c;要想让大模型按照用户的指令执行&#xff0c;Prompt设计是一门艺术&#xff0c;由此还催生了一个职业”Prompt工程师“。其实&#xff0c;并不是所有人都可以设计出好的Prompt&#xff0c;甚至同样的Prompt应用在不同的大模型上表现的结果…

Mysql数据库 20.DCL数据控制语言

因这类SQL语言开发人员操作较少&#xff0c;主要是数据库管理员&#xff08;DBA&#xff09;使用&#xff0c;所以前文没有提及&#xff0c;这篇文章进行补充说明 DCL数据控制语言 用来管理数据库用户&#xff0c;控制数据库的访问权限 1.管理用户 1.1 查询用户 select * f…