C++内存池的简单实现

 更新时间:2021年7月13日 15:00  点击:1854

一、内存池基础知识

1、什么是内存池

1.1 池化技术

池化技术是计算机中的一种设计模式,主要是指:将程序中经常要使用的计算机资源预先申请出来,由程序自己管理,程序在使用时直接从“池”中获取,不仅保证了程序占有的资源数量同时减少资源的申请和释放时间。常见的池化技术有内存池、线程池、连接池等。

1.2 内存池

内存池是一种动态内存分配与管理技术。它的核心思想是:预先申请一段内存空间,使用一种高效的数据结构(哈希、链表)进行管理,当程序需要内存时直接从内存池中分配一块内存给程序,同样当使用完时在归还给内存池。这样做的好处是,减少直接使用new/delete、malloc/free等API申请和释放内存的时间,提高程序运行效率;同时,程序每次直接使用new/delete、malloc/free从内存中申请空间,会导致内存碎片问题,内存池直接申请大块内存就减少了内存碎片。

2、内存池的作用

2.1 效率问题

通常申请内存都是通过new/delete、malloc/free接口直接从内存的堆区申请一块内存,释放也是直接释放到堆中。频繁的申请和释放必然消耗大量时间,降低程序的运行效率。

例如:假设每个链表的节点大小为16字节,当链表需要经常插入节点时,必然就需要频繁的内存申请操纵,每次从堆中申请16个字节都要一定的时间开销,释放内存也需要时间开销。使用内存池,我们可以直接从内存中申请“一批节点”,当程序需要内存时不用直接去堆中申请,直接将预先申请好的内存分配给程序。

2.2 内存碎片

频繁的从内存中申请小块内存会导致内存碎片问题。内存碎片分为内碎片和外碎片两种。

1)外碎片

外碎片也就是我们常说的内存碎片。例如:我们每次从内存中申请一块16字节大小的内存,内存中就会存在很多16个字节大小的块,当该内存释放时就可能造成内存碎片,如下图:

内存中空闲内存大小为88字节,但是我们能申请的最大内存块为21字节。

2)内碎片

内碎片是指已经分配出去的内存中存在的未使用的小块内存。内存池技术虽然解决了内存随便但是又造成了内碎片问题,内碎片不可避免但是可以通过程序的优化减少内存内碎片。

例如:实际需要是申请10byte的内存,定长内存池可能会进行内存对齐,一次性分配了16个字节的内存,多余的6字节实际并未使用,这6字节就是内存内碎片。

3、内存池技术的演进

1)最最最最“简单”的内存池

做一个链表,指向空闲的内存。分配就是从链表中取出来一块返回pop,释放就是将内存在push到链表中。需要做好归并,标记和保护,防止内存二次释放问题。

2)定长内存池

实现一个FreeList类,它的本质是一个链表,节点是一块固定大小的内存,采用头插和头删的方式申请释放内存。每个固定内存分配器里面有两个链表:OpenList用于存储未分配的空闲内存对象(FreeList对象),CloseList用于存储已经分配的内存对象。

分配内存就是从IOpenLsit中取出一个对象给程序,释放内存就是将对象push到CloseList里。当内存不够时,OpenList申请一个大块内存在切割成固定的长度大小的小块内存。

3)C++STL库中的内存池

定长内存池存在的问题就是只能申请固定长度的内存,而实际中我们需要申请的内存大小可能是不管固定,在C++STL库中,采用哈希表和定长内存池结合的方式实现了一个内存池。具体如下

构造多个定长内存池,以一个固定的对齐数进行对齐(例如以8字节进行对齐),第一个定长内存池的内存对象大小为8(至少得能保证无论在64位还是32位系统下都可以保存下一个指针类型),第二个内存池对象大小为16...最后一个内存池对象大小为128byte,当申请的内存大小超过128字节时,通过二级空间配置器申请(直接从内存中申请)。

构造一个哈希表,将不同大小的内存对象挂在哈希表中。如下图:

申请内存:加入要申请的内存大小为8字节直接在index  = 0处分配一块内存,当然申请的内存小于8字节时也会直接分配8字节的内存。当Free_list[index]为nullptr时从内存中申请一块内存,切割成固定大小‘挂在'Free_list[index]位置。

释放内存:根据内存对象大小,计算index在插入到哈希表中的index位置。

二、简易内存池原理

1、整体设计

1.1 内存池结构

两个链表,RequestMemory和ReleaseMemory。

RequestMemory链表存储的是使用new或者malloc从物理内存申请的还没有被使用的内存块,是一个个的memNode节点。

ReleaseMemory链表存储的是使用完释放回来的固定大小的内存块。

1.2 申请内存

  •  先在ReleaseMemory找,如果有内存则直接pop使用
  • ReleaseMemory为nullptr时,在RequestMemory中找。
  • RequestMemory的头节点表示的是新申请的,申请内存时只需要在头结点中找,判断头结点的useCount和sumCount是否相等。当useCount等于sumCount时表示已经用完了,就需要去物理内存中申请,否则直接从表头push一块。
  • 去物理内存申请内存时,申请的大小是上一次申请内存块大小的二倍,并将申请的内存块push到RequestMemory头部。

1.3 释放内存

释放内存时,直接将要释放的内存push到ReleaseMemory的头部即可。

2、详细剖析

2.1 blockNode结构

blockNode表示一个个新申请的内存块,用一个结构体进行管理。blockNode成员如下:

  • void* _memory:表示新申请的内存块的首地址
  • BlockNode * _next:存储next节点
  • _objNum:内存块对象的个数

注意:blockNode的大小每次都是上一次的二倍,是一个质数增长,因此应该设置一个上限,当到达一定大小后进行线性增长。这里规定,最大内存块的大小为100000*sizeof(T),T表示的是申请的节点类型。

2.2 单个对象的大小

这里的单个对象指的ReleaseMemory的节点大小,当用户申请的内存大小sizeof(T)小于sizeof(T*)时,为了能够将该对象链接到ReleaseMemory中,应该按照T*进行分配。

3、性能比较

分别使用malloc/free、new/delete、memPool申请和释放110000个内存,时间如下:

三、简易内存池完整源码

#include<iostream>
#include<vector>
#include<ctime>
using namespace std;
 
template<class T>
class MemPool
{
private:
	//内存块结构
	typedef struct BlockNode
	{
		void* _memory;//内存块地址
		BlockNode* _next;//下一个blockNode
		size_t _objNum;//内存块对象的个数
		//构造函数---num表示申请对象的个数
		BlockNode(size_t num)
			:_objNum(num),
			_next(nullptr)
		{
			_memory = malloc(_objNum*_size);
		}
 
		~BlockNode()
		{
			free(_memory);
			_memory = nullptr;
			_next = nullptr;
			_objNum = 0;
		}
	}BlockNode;
protected:
	static size_t _size;//单个对象的大小
	T* _releaseMemory = nullptr;//释放的内存
	BlockNode* _requestMemory;//申请的内存块
	size_t _maxNum;//内存块最大的大小
	size_t _useCount;//当前内存块已经使用的对象个数
protected:
	//设置单个对象的大小
	static size_t setSize()
	{
		return (sizeof(T) >= sizeof(T*) ? sizeof(T):sizeof(T*));
	}
public:
	MemPool()
		:_useCount(0),
		_releaseMemory(nullptr),
		_maxNum(100000*_size)
	{
		//开始先申请32个_size大小的空间
		_requestMemory = new BlockNode(32);
	}
 
	~MemPool()
	{
		BlockNode *cur = _requestMemory;
		while (cur)
		{
			BlockNode* del = cur;
			cur = cur->_next;
			delete del;            //会自动调用~BlockNode()
		}
	}
 
	T* New()
	{
		//先在releaseMemory中找
		if (_releaseMemory)
		{
			T* obj = _releaseMemory;
			_releaseMemory = *((T**)_releaseMemory);//releaseMemory的前几个字节存储的是下一个节点的地址
			return obj;
		}
		else
		{
			//判断requesetMemory中是否还有空闲内存
			if (_requestMemory->_objNum == _useCount)
			{
				//取物理内存中申请一块内存
				size_t size = 2 * _useCount >= _maxNum ? _maxNum : 2 * _useCount;
				BlockNode* newBlock = new BlockNode(size);
 
				newBlock->_next = _requestMemory;
				_requestMemory = newBlock;
				_useCount = 0;
			}
			//走到这里,一定有内存
			T* obj = (T*)((char*)_requestMemory->_memory+_useCount*_size);
 
			_useCount++;
			return new(obj)T();//用定位new对这块空间初始化
		}
	}
 
	void Delete(T* obj)
	{
		if (obj)
		{
			obj->~T();
 
			*((T**)obj) = _releaseMemory;
			_releaseMemory = obj;
		}
	}
};
 
//静态成员变量,类外初始化
template<typename T>
size_t MemPool<T>::_size = MemPool<T>::setSize();
 
struct TreeNode
{
	int _val;
	TreeNode* _left;
	TreeNode* _right;
};
void test1()
{
	MemPool<TreeNode> mp;
 
	vector<TreeNode*> v;
	for (int i = 0; i < 10; i++)
	{
		TreeNode* mem = mp.New();
		v.push_back(mem);
	}
 
	for (int i = 0; i < 10; i++)
	{
		mp.Delete(v[i]);
	}
}

到此这篇关于C++内存池的简单实现的文章就介绍到这了,更多相关C++ 内存池内容请搜索猪先飞以前的文章或继续浏览下面的相关文章希望大家以后多多支持猪先飞!

[!--infotagslink--]

相关文章

  • C++ STL标准库std::vector的使用详解

    vector是表示可以改变大小的数组的序列容器,本文主要介绍了C++STL标准库std::vector的使用详解,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2022-03-06
  • C++中取余运算的实现

    这篇文章主要介绍了C++中取余运算的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-02-23
  • 详解C++ string常用截取字符串方法

    这篇文章主要介绍了C++ string常用截取字符串方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-04-25
  • C++调用C#的DLL程序实现方法

    本文通过例子,讲述了C++调用C#的DLL程序的方法,作出了以下总结,下面就让我们一起来学习吧。...2020-06-25
  • C++中四种加密算法之AES源代码

    本篇文章主要介绍了C++中四种加密算法之AES源代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。...2020-04-25
  • C++ 整数拆分方法详解

    整数拆分,指把一个整数分解成若干个整数的和。本文重点给大家介绍C++ 整数拆分方法详解,非常不错,感兴趣的朋友一起学习吧...2020-04-25
  • C++中 Sort函数详细解析

    这篇文章主要介绍了C++中Sort函数详细解析,sort函数是algorithm库下的一个函数,sort函数是不稳定的,即大小相同的元素在排序后相对顺序可能发生改变...2022-08-18
  • C++万能库头文件在vs中的安装步骤(图文)

    这篇文章主要介绍了C++万能库头文件在vs中的安装步骤(图文),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-02-23
  • 详解C++ bitset用法

    这篇文章主要介绍了C++ bitset用法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-04-25
  • 浅谈C++中的string 类型占几个字节

    本篇文章小编并不是为大家讲解string类型的用法,而是讲解我个人比较好奇的问题,就是string 类型占几个字节...2020-04-25
  • C++ Eigen库计算矩阵特征值及特征向量

    这篇文章主要为大家详细介绍了C++ Eigen库计算矩阵特征值及特征向量,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2020-04-25
  • VSCode C++多文件编译的简单使用方法

    这篇文章主要介绍了VSCode C++多文件编译的简单使用方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2021-03-29
  • C++ pair的用法实例详解

    这篇文章主要介绍了C++ pair的用法实例详解的相关资料,需要的朋友可以参考下...2020-04-25
  • C++中的循环引用

    虽然C++11引入了智能指针的,但是开发人员在与内存的斗争问题上并没有解放,如果我门实用不当仍然有内存泄漏问题,其中智能指针的循环引用缺陷是最大的问题。下面通过实例代码给大家介绍c++中的循环引用,一起看看吧...2020-04-25
  • C++随机点名生成器实例代码(老师们的福音!)

    这篇文章主要给大家介绍了关于C++随机点名生成器的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-04-25
  • C++如何删除map容器中指定值的元素详解

    map容器是C++ STL中的重要一员,删除map容器中value为指定元素的问题是我们经常与遇到的一个问题,下面这篇文章主要给大家介绍了关于利用C++如何删除map容器中指定值的元素的相关资料,需要的朋友可以参考借鉴,下面来一起看看吧。...2020-04-25
  • C++ 约瑟夫环问题案例详解

    这篇文章主要介绍了C++ 约瑟夫环问题案例详解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下...2021-08-15
  • C++中cin的用法详细

    这篇文章主要介绍了C++中cin的用法详细,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-04-25
  • 基于C++中常见编译错误的总结详解

    本篇文章是对C++中的常见编译错误进行了详细的分析介绍,需要的朋友参考下...2020-04-25
  • c++优先队列(priority_queue)用法详解

    这篇文章主要介绍了c++优先队列(priority_queue)用法详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-04-25