【C++】内存池

目录

一、什么是内存池

1.池化技术

2.内存池

3.内存池主要解决的问题

二、内存池的实现

1.New申请空间

2.Delete释放空间

3.再看New申请空间

  

4.内存池完整代码

三、内存池性能测试


一、什么是内存池

1.池化技术

所谓 "池化技术",就是程序向系统申请过量的资源,然后⾃⼰管理,以备不时之需。之所以要申请过量的资源,是因为每次申请该资源都有较⼤的开销,不如提前申请好了,这样使⽤时就会变得⾮常快捷,⼤⼤提⾼程序运⾏效率.

在计算机中,有很多使⽤“池”这种技术的地⽅,除了内存池,还有连接池、线程池、对象池等。以服务器上的线程池为例,它的主要思想是:先启动若⼲数量的线程,让它们处于睡眠状态,当接收到客⼾端的请求时,唤醒池中某个睡眠的线程,让它来处理客⼾端的请求,当处理完这个请求,线程⼜进⼊睡眠状态。


2.内存池

内存池是指程序预先从操作系统申请⼀块⾜够⼤内存,此后,当程序中需要申请内存的时候,不是直接向操作系统申请,⽽是直接从内存池中获取;同理,当程序释放内存的时候,并不真正将内存返回给操作系统,⽽是返回内存池。当程序退出(或者特定时间)时,内存池才将之前申请的内存真正释放。


3.内存池主要解决的问题

内存池主要解决的当然是效率的问题。

举个很形象的例子。上大学了,需要生活费了,然后呢,你每天要花假如50块钱,然后你就每天早上给你妈妈打电话要50,一个月就要打30次电话,你妈妈觉得烦死了,你妈妈说这样,我明天给你1500,之后每过1个月我在给你钱,一次就给你1个月的,至于怎么花你自己看着办。

在上面的故事中显然是一次给你1500块,效率会更高。对于操作系统也是一样的,每一次调用malloc申请空间都是有消耗的,还不如我一次给你一大块空间,你自己对这块空间进行一个管理,这样就使得效率大大提升。

二、内存池的实现

我们要写一个内存池,那首先需要用到模版,模版的作用就是告诉我你每次要申请多大块的内存空间。

其次我们当然需要用类封装起来,类的名字就叫ObjectPool,对象池的意思,然后我们成员函数需要什么呢?既然是申请内存的,那肯定需要一个指针_memory,来指向大块的空间,那这个指针是什么类型呢?char?int?还是其它的类型呢?用什么类型我们要明白指针的类型意味着什么,指针的类型意味着对这个指针进行++操作会往后走多少个字节,还有就是解引用可以看到的空间大小,比如说是int类型的指针,进行++操作就会往后走4个字节,解引用就是看4字节的空间。那么就要结合我们内存池的功能来看了,当一个对象申请空间之后,比如说这个对象要申请10字节空间,就意味着我们要从_memory中切10字节的空间给它,那请问,如果是int类型,好不好切?不好切,难道强转一下吗?最好用的还是char类型,因为只有1个字节,你要10字节空间,直接_memory往后加上10就行了。最开始初始化为nullptr。

还需要什么成员呢?当一个对象把内存还回来,我们直接释放吗?当然不是,我们可以连接在一个链表上,然后等后面其它的对象申请空间的时候将挂在链表上的内存给它们使用,那这个链表用一个结构体吗?然后里面定义一个next,一个data?不需要,直接用一个指针_freeList就可以,我们可以让这个指针的头4个字节(或者8个)存下一个内存块的地址,每次还回来一个内存块就让前一个内存块的头4个字节存还回来的内存块地址,最后一个内存块的前4个字节存nullptr。这里也可能是8个字节,如果是64位平台的话。那这个链表什么类型呢?void*就可以。最开始初始化为nullptr。

template<class T>

class ObjectPool
{
public:

private:
	char* _memory;
    void* _freeList;
};

有了成员变量接下来就要实现功能了,功能是啥呢?无非就是申请和释放内存,下面我们来看看,来个一个对象要申请空间,我们怎么申请。

1.New申请空间

T* New()
	{
		if (_memory == nullptr)
		{
			_memory = (char*)malloc(128 * 1024); // 第一次申请,给128K的大块内存
			if (_memory == nullptr)
			{
				throw std::bad_alloc(); //抛异常
			}
		}

		T* obj = (T*)_memory;
		_memory += sizeof(T);

		return obj;
	}

函数名就叫New吧,需要参数吗,不需要,返回值是需要的,把申请的内存空间返回给调用的对象,刚开始的时候,_memory还是空的,因此,当_memory为空说明第一次申请,此时要给_memory开一大块内存,这里我们直接开128K的内存,然后直接将_memory强转为T*赋值给obj即可,T*类型表明解引用后可以看sizeof(T)大小的空间,然后让_memory往后加上T对象的大小。此时就切出去了一个T对象,然后直接把obj返回即可。但是这样写是对的吗?

比如说我切到最后一块,把_memory切完了,_memory加加往后走到了不属于自己的空间,然后又要来申请内存,此时_memory是空吗?不是空呀,虽然后面的内存不是你的,但也不是空,此时你能申请吗?你不能,但我怎么知道_memory有没有被切空?此时要加一个成员变量remainBytes,表示当前的内存块剩余的字节数。

那么此时的代码就需要做改动了,此时我们申请大块内存的判断条件就不应该是_memory为空了,而应该是remainBytes等于0的时候,但是remainBytes==0这个判断条件是对的吗?其实是不对的,因为有可能我申请的对象大小不能被整除,比如说这个对象每次都要5字节,但_memory只有128字节,因为5不能被128整除,因此等到_memory被申请到最后一块的时候只剩下3字节了,此时这个对象在申请空间,但是我们只有3字节,不够一个对象的大小,此时也需要重新开一块大内存然后给对方。因此这里开辟大块内存的判断条件是remainBytes< sizeof(T)的时候,当不够一个对象的大小,就申请空间。

因此我们的代码应该这样写。

#include <iostream>
using std::cout;
using std::endl;

template<class T>

class ObjectPool
{
public:
		T* New()
	{ 
		// 当剩余的内存不够一个对象大小的时候,就重新开大块空间
		if (remainBytes < sizeof(T))
		{
			_memory = (char*)malloc(128 * 1024); // 第一次申请,给128K的大块内存
			if (_memory == nullptr)
			{
				throw std::bad_alloc(); //抛异常
			}
			remainBytes = 128 * 1024;
		}

		T* obj = (T*)_memory;
		_memory += sizeof(T);
		remainBytes -= sizeof(T);
		return obj;
	}

private:
	size_t remainBytes = 0;	   // 大块内存切分过程中剩余的字节数
	char* _memory = nullptr;   // 指向大块内存的指针
	void* _freeList = nullptr; // 管理还回来的内存块
};

2.Delete释放空间

下面我们再来写对象把内存还回来的代码。

函数名就叫Delete,不需要返回值,参数当然是需要的,把你要还回来的内存给我传过来。

你把内存给我还回来了,那我就要对还回来的内存进行管理,链接到自由链表_freeList上,如果是第一次还回来,_freeList为nullptr,那我们就需要让_freeList指向还回来的obj,然后把_freeList的前4个字节处放一个nullptr,怎么做呢?直接把_freeList强转成int*类型,然后对_freeList进行解引用,int类型解引用看到的就是4个字节,然后把NULL赋值给_freeList即可。

这样写在32位下是没问题的,但64位下是跑不动的,64位下指针的大小是8个字节,int*的指针解引用只有4个字节,那怎么办呢?我们可以判断一下,如果sizeof(int*)的大小为4,就执行上面的逻辑,否则,就是8个字节,那我们直接*(long long*)obj就好了。

这样写当然是可以,但是还有一种很牛的写法,直接把obj强转为void**,然后解引用。对int*解引用看到的是int大小的空间,对void**解引用看到的是void*大小的空间,void*是什么?是个指针,指针的大小在32位下是4字节,64位下是8字节,是不是很巧妙?这里void**根本不重要,即使是int**、char**都可以,因为指针的大小是固定的。

如果不是第一次我们直接采取头插的方式,让还回来的内存块obj的前几个字节(4或8)存_freeList的地址,此时obj就指向了_freeList,然后再把obj给给_freeList即可。

然而上面的第一次申请和后续的申请可以合并为一种,就是我不管什么情况直接头插,第一次的时候_freeList是空,此时obj前几个字节就指向nullptr,然后再把obj赋值给_freeList,是不是也没问题。

void Delete(T* obj)
	{
		// 头插
		*(void**)obj = _freeList;
		_freeList = obj;
	}

3.再看New申请空间

有没有这样一种可能,就是一直申请空间,然后切_memory,切的时候也会还一些回来,然后_memory被切空了,按照我们上面的逻辑,此时是不是会重写申请大块空间,但是申请的时候也有可能还回来,就意味着_freeList还存了很多内存块没用,但我们还是去申请大块内存了,此时这_freeList上的内存不就浪费了吗?因此,向我们要空间的时候,我们应该优先把_freeList上的内存块切出去给对方,这样是不是才更合理呢?不然管理_freeList干嘛。

我们先把_freeList指向的下一个内存块存在next里,然后直接把头上的_freeList给给obj,然后再让_freeList指向next,最后把obj返回即可。_freeList的下一个内存块地址在头部前几个字节里面存着。

	T* New()
	{ 
		T* obj = nullptr;
		// 自由链表有内存块,优先去自由链表申请内存
		if (_freeList)
		{
			void* next = *(void**)_freeList; 
			obj = (T*)_freeList;
			_freeList = next;
			return obj;
		}
		else
		{
			// 当剩余的内存不够一个对象大小的时候,就申请空间
			if (remainBytes < sizeof(T))
			{
				_memory = (char*)malloc(128 * 1024); // 第一次申请,给128K的大块内存
				if (_memory == nullptr)
				{
					throw bad_alloc(); //抛异常
				}
				remainBytes = 128 * 1024;
			}

			obj = (T*)_memory;
			_memory += sizeof(T);
			remainBytes -= sizeof(T);

			return obj;
		}		
	}

  

我们来梳理一下我们的申请空间代码,如果_freeList自由链表有内存块就去_freeList里面申请,如果没有就去内存块_memory里申请,_memory剩余字节数足够一个对象大小,直接切出去,如果不够要申请大块的内存块,再去_memory里申请。

问题1:

此时还有一个问题,那就是可能我这个申请的对象只有3个字节,我们直接切一块空间给它,然后这个3字节的对象还回来的时候就有问题了,有啥问题呢?还回来的时候要链接到_freeList上,怎么连接呢?头插呀,前几个自己存_freeList的地址,然后_freeList在指向obj,但我obj只有3个字节,32位下地址也要4字节,更别提64位了,我根本放不下呀,因此在申请空间的时候,如果对象的字节数不够一个指针类型的大小,我们要补够。

因此直接这样,当T小于一个指针大小,32位4,64位8, 让objSize位一个指针大小,否则就为T的大小,然后让_memory直接 += objSize即可。

比如果T是3个字节,在32位下,我们还是给它申请3字节的空间,但是_memory往后走的时候就要往后走4个字节。此时还回来的时候虽然T是3个字节,但我们是4字节4字节切分的,因此3字节的后面1个字节也是属于它的,因此直接存地址即可。

问题2:
这里我们只是开了空间,但是没有初始化,我们只是还回来了空间但是没有把空间里的内容清理掉。因此我们可以用定位new,显示调用T的构造函数初始化,释放的时候显示调用一下T的析构函数清理对象。

此时这个对象池就比较完美了。 

4.内存池完整代码

#include <iostream>
using std::cout;
using std::endl;

template<class T>

class ObjectPool
{
public:
	T* New()
	{ 
		T* obj = nullptr;
		// 自由链表有内存块,优先去自由链表申请内存
		if (_freeList)
		{
			void* next = *(void**)_freeList;
			obj = (T*)_freeList;
			_freeList = next;
		}
		else
		{
			// 当剩余的内存不够一个对象大小的时候,就申请空间
			if (remainBytes < sizeof(T))
			{
				_memory = (char*)malloc(128 * 1024); // 第一次申请,给128K的大块内存
				if (_memory == nullptr)
				{
					throw std::bad_alloc(); //抛异常
				}
				remainBytes = 128 * 1024;
			}

			obj = (T*)_memory;
			size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
			_memory += objSize;
			remainBytes -= sizeof(T);
		}	
		// 定位new,显示调用T的构造函数初始化
		new (obj)T;
		return obj;

	}

	void Delete(T* obj)
	{
		// 显示调用析构函数清理对象
		obj->~T();
		// 头插
		*(void**)obj = _freeList;
		_freeList = obj;
	}

private:
	size_t remainBytes = 0;	   // 当前内存块剩余的字节数
	char* _memory = nullptr;   // 指向大块内存的指针
	void* _freeList = nullptr; // 管理还回来的内存块
};

三、内存池性能测试

#include "MemPool.hpp"
#include <vector>

struct TreeNode
{
	int _val;
	TreeNode* _left;
	TreeNode* _right;
	TreeNode()
		:_val(0)
		, _left(nullptr)
		, _right(nullptr)
	{}
};
void TestObjectPool()
{
	// 申请释放的轮次
	const size_t Rounds = 3;
	// 每轮申请释放多少次
	const size_t N = 100000;

	size_t begin1 = clock();
	std::vector<TreeNode*> v1;
	v1.reserve(N);
	for (size_t j = 0; j < Rounds; ++j)
	{
		for (int i = 0; i < N; ++i)
		{
		v1.push_back(new TreeNode);
		}
		for (int i = 0; i < N; ++i)
		{
		delete v1[i];
		}
		v1.clear();
	}
	size_t end1 = clock();

	ObjectPool<TreeNode> TNPool;

	size_t begin2 = clock();
	std::vector<TreeNode*> v2;
	v2.reserve(N);
	for (size_t j = 0; j < Rounds; ++j)
	{
		for (int i = 0; i < N; ++i)
		{
			v2.push_back(TNPool.New());
		}
		for (int i = 0; i < 100000; ++i)
		{
			TNPool.Delete(v2[i]);
		}
		v2.clear();
	}
	size_t end2 = clock();

	cout << "new cost time:" << end1 - begin1 << endl;
	cout << "object pool cost time:" << end2 - begin2 << endl;
}


int main()
{
	TestObjectPool();
}

上面的代码我相信大家一看就知道在干嘛吧。 

这里我们可以看到,我们内存池的效率比malloc块了基本上2倍,new的底层也是malloc。 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

计算机新手练级攻略——如何搜索问题

目录 计算机学生新手练级攻略——如何搜索问题1.明确搜索意图2.使用精确关键词3.使用专业引擎搜索4.利用好技术社区1. Stack Overflow2. GitHub3. IEEE Xplore4. DBLP 5.使用代码搜索工具1. GitHub 代码搜索2. Stack Overflow 代码搜索3. Papers with Code4. IEEE Xplore 6.查阅…

区块链技术在电子政务中的应用

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 区块链技术在电子政务中的应用 区块链技术在电子政务中的应用 区块链技术在电子政务中的应用 引言 区块链技术概述 定义与原理 发…

stm32 踩坑笔记

串口问题&#xff1a; 问题&#xff1a;会改变接收缓冲的下一个字节 串口的初始化如下&#xff0c;位长度选择了9位。因为要奇偶校验&#xff0c;要选择9位。但是接收有用数据只用到1个字节。 问题原因&#xff1a; 所以串口接收时会把下一个数据更改

034集——JIG效果实现(橡皮筋效果)(CAD—C#二次开发入门)

可实现效果如下&#xff08;对象捕捉F3需打开&#xff0c;否则效果不好&#xff09;&#xff1a; public class CircleJig : EntityJig{public static void DraCJig(){PromptPointResult ppr Z.ed.GetPoint("a");if (ppr.Value null) return;Point3d pt ppr.Value…

数据资产入表,如何接住这“泼天的富贵”?

很多管理者没有意识到&#xff0c;数据资产入表是企业增加资产的一场“开卷考试”。 “数据资产入表”&#xff0c;指在企业的资产负债表上体现数据资产&#xff0c;在法律上认可数据资产的财务价值。去年财政部发布《企业数据资源相关会计处理暂行规定》&#xff0c;并于今年…

更稳更高效!大道云行助力广电业务腾飞!

重庆广播电视集团成立于2004年11月&#xff0c;旗下拥有5套广播频率、13套电视频道、覆盖全市3300万人口的有线、无线传输网络&#xff0c;以及由第1眼新闻、视界网、官方微信微博群等组成的新媒体矩阵&#xff0c;融合传播综合实力位居全国前列。 目前&#xff0c;重庆广电全…

NIST密码学未来展望:Naughty Step 上的 SHA-1、3DES 和 SHA-224

1. 引言 NIST 几十年来一直致力于推动密码学标准的发展&#xff0c;2024年10月&#xff0c;其发布了Transitioning the Use of Cryptographic Algorithms and Key Lengths 草案&#xff1a; 概述了 SHA-1&#xff08;为160位哈希算法&#xff09; 将在不久的将来退役&#xf…

物理验证Calibre LVS | SMIC Process过LVS时VNW和VPW要如何做处理?

SMIC家工艺的数字后端实现PR chipfinish写出来的带PG netlist如下图所示。我们可以看到标准单元没有VNW和VPW pin的逻辑连接关系。 前几天小编在社区星球上分享了T12nm ananke_core CPU低功耗设计项目的Calibre LVS案例&#xff0c;就是关于标准单元VPP和VBB的连接问题。 目前…

今天给在家介绍一篇基于jsp的旅游网站设计与实现

项目描述 临近学期结束&#xff0c;还是毕业设计&#xff0c;你还在做java程序网络编程&#xff0c;期末作业&#xff0c;老师的作业要求觉得大了吗?不知道毕业设计该怎么办?网页功能的数量是否太多?没有合适的类型或系统?等等。这里根据疫情当下&#xff0c;你想解决的问…

【C#设计模式(4)——构建者模式(Builder Pattern)】

前言 C#设计模式(4)——构建者模式(Builder Pattern) 运行结果 代码 public class Computer {private string part1 "CPU";private string part2 "主板";private string part3 "内存";private string part4 "显卡";private st…

软件测试第二篇软件测试技术

第五章单元测试和集成测试的技术 单元静态测试主要由开发人员完成。 标准&#xff1a;规定什么能做&#xff0c;什么不能做。 规范&#xff1a;建议你要怎么做。 5.1.2 代码评审 代码评审是一种发现代码缺陷的另一种测试方法。 代码审查的最佳实践&#xff1a; 创建代码审…

【Android、IOS、Flutter、鸿蒙、ReactNative 】文本点击事件

Android Studio 版本 Android Java TextView 实现 点击事件 参考 import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.TextView; import android.widget.Toast;public c…

二叉树(C 语言)

目录 一、树1. 树的概念2. 树的表示方法3. 树在实际当中的应用 二、二叉树1. 二叉树的定义2. 现实中的二叉树3. 特殊的二叉树4. 二叉树的性质5. 二叉树的存储结构 三、堆 —— 完全二叉树的顺序存储1. 堆的概念2. 堆的性质3. 堆的设计思路4. 堆的实现代码 四、堆排序1. 堆排序的…

游戏引擎学习第五天

这节貌似没讲什么 视频参考:https://www.bilibili.com/video/BV1Gmm2Y5EwE/ uint8 *A somewhere in memory; uint8 *B somewhere in memory;//BEFORE WE GOT TO HERE int Y *B; // whatever was actually there before the 5 *A 5; int X *B; // 5 //Obviously! Y and …

大路灯护眼灯十大品牌哪个牌子好?儿童大路灯护眼灯品牌排行榜

大路灯护眼灯十大品牌哪个牌子好&#xff1f;长时间在不良光线下用眼很容易引起视觉疲劳&#xff0c;最终影响视力健康&#xff0c;这个时候大路灯护眼灯以良好的性能成为了很不错的照明产品。不过如今行业热度很高&#xff0c;网红跨界品牌大路灯护眼灯出于成本压缩&#xff0…

1.2 图像处理基本操作

在本实战中&#xff0c;我们将学习如何使用OpenCV进行基本的图像处理操作。首先&#xff0c;我们将通过cv2.imread()函数读取图像&#xff0c;并使用cv2.imshow()在窗口中显示它。接着&#xff0c;我们将探索如何通过cv2.imwrite()保存图像&#xff0c;并设置不同的参数以控制图…

K8S如何基于Istio实现全链路HTTPS

K8S如何基于Istio实现全链路HTTPS Istio 简介Istio 是什么?为什么选择 Istio?Istio 的核心概念Service Mesh(服务网格)Data Plane(数据平面)Sidecar Mode(边车模式)Ambient Mode(环境模式)Control Plane(控制平面)Istio 的架构与组件Envoy ProxyIstiod其他组件Istio 的流量管…

手动搭建 Ghost 博客

操作场景 Ghost 是使用 Node.js 语言编写的开源博客平台&#xff0c;您可使用 Ghost 快速搭建博客&#xff0c;简化在线出版过程。本文档介绍如何在腾讯云云服务器&#xff08;CVM&#xff09;上手动搭建 Ghost 个人网站。 进行 Ghost 网站搭建&#xff0c;您需要熟悉 Linux …

MySQL之索引(3)(索引基本语法、SQL执行计划、常见索引失效原因与解决方法)

目录 一、索引基本语法。 &#xff08;1&#xff09;创建索引。 &#xff08;2&#xff09;查看索引。 &#xff08;3&#xff09;删除索引。 &#xff08;4&#xff09;给多列添加组合索引。 1、何时添加索引&#xff1f;&#xff1f; 2、组合索引。 二、SQL执行计划。 &#…

前端中的 File 和 Blob两个对象到底有什么不同

JavaScript 在处理文件、二进制数据和数据转换时&#xff0c;提供了一系列的 API 和对象&#xff0c;比如 File、Blob、FileReader、ArrayBuffer、Base64、Object URL 和 DataURL。每个概念在不同场景中都有重要作用。下面的内容我们将会详细学习每个概念及其在实际应用中的用法…