浅谈STL中的分配器

分配器是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.mzph.cn/news/187399.shtml

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

相关文章

rest_framework_django 学习笔记二(视图路由)

rest_framework_django 学习笔记二&#xff08;视图路由&#xff09; rest_framwork_django学习笔记一(序列化器) 一、rest framework 中Request 与 Response 1、Request REST framework 传入视图的request对象不再是Django默认的HttpRequest对象&#xff0c;二是REST Fame…

3D模型渲染导致电脑太卡怎么办?

在线工具推荐&#xff1a; 三维数字孪生场景工具 - GLTF/GLB在线编辑器 - Three.js AI自动纹理化开发 - YOLO 虚幻合成数据生成器 - 3D模型在线转换 - 3D模型预览图生成服务 1、什么是3D渲染&#xff1f; 3D渲染是指通过计算机图形学技术将三维模型转化为二维图像的过程…

人工智能“排头兵”,探访福州多地 AI 智算实践

生成式 AI 在 2023 年再次引爆 IT 技术发展&#xff0c;福建作为数字中国的重要策源地&#xff0c;也是国家数字经济创新发展试验区&#xff0c;在人工智能方面拥有良好的产业基础和人才优势&#xff0c;同时近期出台的《福建省促进人工智能产业发展十条措施》&#xff0c;为福…

带键扫的LED专用驱动方案

一、基本概述 TM1650 是一种带键盘扫描接口的LED&#xff08;发光二极管显示器&#xff09;驱动控制专用电路。内部集成有MCU输入输出控制数字接口、数据锁存器、LED 驱动、键盘扫描、辉度调节等电路。TM1650 性能稳定、质量可靠、抗干扰能力强&#xff0c;可适用于24 小时长期…

docker buildx跨架构构建笔记(x86_64构建下构建aarch64镜像)

docker buildx跨架构构建(x86_64构建aarch64镜像) 文章目录 docker buildx跨架构构建(x86_64构建aarch64镜像)简介第一步 先交叉编译一个aarch64的HelloWorld程序。准备一个用于跨架构的Dockerfile文件使用docker buildx命令构建aarch64架构的镜像。查看镜像具体详细信息&#…

Linux常用命令——vi命令

文章目录 vi的工作模式常用快捷键提示和技巧结论 Linux环境下的vi编辑器不仅以其强大的功能著称&#xff0c;也因其快捷键而闻名。这些快捷键可以显著提高编辑效率&#xff0c;是每个使用vi的人必须掌握的。下面将扩展介绍vi的一些常用快捷键。 vi的工作模式 vi主要有两种模式…

【机器学习】线性模型之逻辑回归

文章目录 逻辑回归Sigmoid 函数概率输出结果预测值与真实标签之间的并不匹配交叉熵逻辑回归模型 梯度下降逻辑回归模型求解编程求解sklearn 实现&#xff0c;并查看拟合指标 逻辑回归 逻辑回归是一种广义线性模型&#xff0c;形式上引入了 S i g m o i d Sigmoid Sigmoid 函数…

Windows10中在Visual Studio2017中VC++项目安装使用GoogleTest库

Windows10中在Visual Studio2017中VC项目安装使用GoogleTest库 在Windows10中VC程序中可以不用自己手动下载GoogleTest源代码&#xff0c;可以直接通过【项目】-> 【管理 NuGet 程序包】-> 【浏览】-> 搜索 googletest&#xff0c; 找到Microsoft.googletest.v140.wi…

物联网实训室虚拟仿真软件建设方案

一、概述 物联网实训室虚拟仿真软件旨在紧密围绕立德树人的根本任务&#xff0c;充分依托先进的数字技术&#xff0c;并对接物联网行业的发展趋势和人才需求。通过对比真实企业工作环境&#xff0c;融合创新创业教育基因&#xff0c;秉承虚拟仿真技术与教育教学深度融合的理念&…

流批一体历史背景及基础介绍

目录 一、历史背景1.BI系统2.传统大数据架构3.流式架构4.Lambda架构5.Kappa架构 二、流批一体与数据架构的关系数据分析型应用数据管道型应用 三、流与批的桥梁Dataflow模型四、Dataflow模型的本质一个基本点两个时间域三个子模型1.窗口模型2.触发器模型3. 增量计算模型 四个分…

Netty Review - 探索Pipeline的Inbound和Outbound

文章目录 概念Server CodeClient CodeInboundHandler和OutboundHandler的执行顺序在InboundHandler中不触发fire方法InboundHandler和OutboundHandler的执行顺序如果把OutboundHandler放在InboundHandler的后面&#xff0c;OutboundHandler会执行吗 概念 我们知道当boss线程监控…

自学MySql(一)

1.安装下载 下载网址 2、将mysql的bin目录添加到环境变量&#xff08;可选&#xff09; 3、使用一下命令测试

GAN:WGAN前作

WGAN前作&#xff1a;有原则的方法来训练GANs 论文&#xff1a;https://arxiv.org/abs/1701.04862 发表&#xff1a;ICLR 2017 本文是wgan三部曲的第一部。文中并没有引入新的算法&#xff0c;而是标是朝着完全理解生成对抗网络的训练动态过程迈进理论性的一步。 文中基本是…

文心一言 VS 讯飞星火 VS chatgpt (146)-- 算法导论12.2 1题

一、用go语言&#xff0c;假设一棵二叉搜索树中的结点在1到 1000 之间&#xff0c;现在想要查找数值为 363 的结点。下面序列中哪个不是查找过的序列? a.2&#xff0c;252&#xff0c;401&#xff0c;398&#xff0c;330&#xff0c;344&#xff0c;397&#xff0c;363。 b.9…

ps 透明印章制作

ps 透明印章制作 1、打开不透明印章2、抠出红色印章3、新建图层4、填充红色印章到新图层5、导出透明印章 1、打开不透明印章 打开ps软件&#xff0c;菜单栏选择 文件-打开 选择本地不透明印章 打开 2、抠出红色印章 ps菜单栏 选择 选择-色彩范围 点击色彩范围 色彩范围窗口 取…

内网协议区别

今天面试的时候被面试官问到内网隧道技术中的协议有什么区别&#xff0c;平时只注重使用不注重原理&#xff0c;学习记录 2023-11-30 网络层&#xff1a;IPV6 隧道、ICMP 隧道、GRE 隧道 传输层&#xff1a;TCP 隧道、UDP 隧道、常规端口转发 应用层&#xff1a;SSH 隧道、HTTP…

基于B/S架构的医院一体化电子病历编辑器源码

电子病历在线制作、管理和使用的一体化电子病历解决方案&#xff0c;通过一体化的设计&#xff0c;提供对住院病人的电子病历书写、保存、修改、打印等功能。电子病历系统将临床医护需要的诊疗资料以符合临床思维的方法展示。建立以病人为中心&#xff0c;以临床诊疗信息为主线…

使用 SDKMAN 管理多版本本地 Java 环境---Centos8 Windows

文章目录 windows 安装centos8 安装卸载sdkman使用 windows 安装 SDKMAN是一个 jdk 多版本管理工具&#xff0c;类似于 nodejs 中的 nvm。可以在本地存在多个 java 环境&#xff0c;快速切换功能&#xff0c;同时&#xff0c;他不止于 java sdk&#xff0c;还有maven、tomcat等…

算法:双指针

数组分块 题型特点&#xff1a;给一个数组&#xff0c;在某个规则下将数组划分成几个区间 解决&#xff1a;双指针&#xff08;数组中利用下标充当指针&#xff09; 283 移动0 定义两个指针 dest指针&#xff08;已处理区间内非0元素的最后一个位置&#xff09;cur指针&#…

Memcached最新2023年面试题,高级面试题及附答案解析

文章目录 01、Memcached是什么&#xff0c;有什么作用&#xff1f;02、Memcached的多线程是什么&#xff1f;如何使用它们&#xff1f;03、Memcached与Redis的区别&#xff1f;04、如果缓存数据在导出导入之间过期了&#xff0c;怎么处理这些数据呢&#xff1f;05、如何实现集群…