C++如何实现定长内存池详解

1. 池化技术

池是在计算机技术中经常使用的一种设计模式,其内涵在于:将程序中需要经常使用的核心资源先申请出来,放到一个池内,由程序自己管理,这样可以提高资源的使用效率,也可以保证本程序占有的资源数量。 经常使用的池技术包括内存池、线程池和连接池(数据库经常使用到)等,其中尤以内存池和线程池使用最多。

2. 内存池概念

内存池(Memory Pool) 是一种动态内存分配与管理技术。 通常情况下,程序员习惯直接使用 new、delete、malloc、free 等API申请分配和释放内存,这样导致的后果是:当程序长时间运行时,由于所申请内存块的大小不定,频繁使用时会造成大量的内存碎片从而降低程序和操作系统的性能。内存池则是在真正使用内存之前,先申请分配一大块内存(内存池)留作备用,当程序员申请内存时,从池中取出一块动态分配,当程序员释放内存时,将释放的内存再放入池内,再次申请池可以 再取出来使用,并尽量与周边的空闲内存块合并。若内存池不够时,则自动扩大内存池,从操作系统中申请更大的内存池。

2.1 内存碎片

  • 内碎片:

内部碎片就是已经被分配出去(能明确指出属于哪个进程)却不能被利用的内存空间;内部碎片是处于区域内部或页面内部的存储块。占有这些区域或页面的进程并不使用这个 存储块。而在进程占有这块存储块时,系统无法利用它。直到进程释放它,或进程结束时,系统才有可能利用这个存储块。(编译器会对数据进行对齐操作,当不是编译器的最小对齐数的整数倍的时候需要添加一些来保证对齐,那么这块为了对齐而添加的就是内碎片)

  • 外碎片(通常所讲的内存碎片):

假设系统依次分配了16byte、8byte、16byte、4byte,还剩余8byte未分配。这时要分配一个24byte的空间,操作系统回收了一个上面的两个16byte,总的剩余空间有40byte,但是却不能分配出一个连续24byte的空间,这就是外碎片问题。(本来有足够的内存,但是由于碎片化无法申请到稍大一些的连续内存)

C++如何实现定长内存池详解_第1张图片

3. 实现定长内存池

3.1 定位new表达式(placement-new)

定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。使用格式:new (place_address) type或者new (place_address) type(initializer-list),place_address必须是一个指针,initializer-list是类型的初始化列表。
使用场景:

定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要**使用new的定义表达式进行显示调构造函数**进行初始化。

3.2 完整实现

即实现一个 FreeList,每个 FreeList 用于分配固定大小的内存块,比如用于分配 32字节对象的固定内存分配器,之类的。

优点:
简单粗暴,分配和释放的效率高,解决实际中特定场景下的问题有效。

缺点:
功能单一,只能解决定长的内存需求,另外占着内存没有释放。

C++如何实现定长内存池详解_第2张图片

实现的思想:

  1. 先向内存申请一块大的内存,如果需要,那么就对这块已经申请出来的内存进行切割(减少了和操作系统底层打交道的次数,效率也就提高了,内存池一定是可以解决申请和释放内存的效率的)
  2. 对于不需要的小块内存,并不是将其进行释放掉,而是使用一个freeList将他们管理起来,如果freeList中有了空余的,那么再次申请内存首先会到自由链表中取,而不是去申请出来的大内存块进行切割
  3. 对于这个申请出来的小块内存,前4个或者8个字节存放的是下一个小内存块的地址(这是由于在32位平台下指针的大小是4字节,在64位平台下指针则是8字节),这里如何巧妙的进行平台下指针大小的适配,需要好好的进行琢磨。
  4. (帮助理解3)指针就是地址,那么指针的类型是为了解引用取到大小,对于所申请出来的内存的类型我是不关心的,在32位平台下我就想取出他的前4个字节,然后存放我的下一个小内存的地址,所以把obj强转为int*类型,然后在解引用就可以拿到前4个字节。那如果在64位平台下,就应该取其前8个字节来存放下一个小内存的地址,但是如果都写为取前4个字节的话,这里就会发生指针越界的问题。下述代码所写的Nextobj()接口函数就是为了能够取出小内存中的前4个字节或者8个字节。我需要的类型是void*,可以自动的适配平台(类比于上述的int类型,就可以相通)
//实现一个定长的内存池(针对某一个具体的对象,所以起名字叫ObjictPool)
#pragma once 

#include"Common.h"

template
class ObjectPool
{
public:
	~ObjectPool()
	{
		//
	}
	//此时代码还存一个很大的问题:我们默认这里取的是前四个字节,但是在64位的平台下,需要取的应该是这块小内存的前8个字节来保存地址
	void*& Nextobj(void* obj)
	{
		return *((void**)obj); //对于返回的void*可以自动的适配平台
	}
	//申请内存的函数接口
	T* New()
	{
		T* obj = nullptr;
		//一上来首先应该判断freeList
		if (_freeList)
		{
			//那就直接从自由链表中取一块出来
			obj = (T*)_freeList;
			//_freeList = (void*)(*(int*)_freeList);
			_freeList = Nextobj(_freeList);
		}
		else
		{
			//表示自由链表是空的
			//那么这里又要进行判断,memory有没有
			if (_leftSize < sizeof(T)) //说明此时空间不够了
			{
				//那么就进行切割
				_leftSize = 1024 * 100;
				_memory = (char*)malloc(_leftSize);
				//对于C++来说,如果向系统申请失败了,则会抛异常
				if (_memory == nullptr)
				{
					throw std::bad_alloc();
				}
			}
			//进行memory的切割
			obj = (T*)_memory;
			_memory += sizeof(T); //这里如果想不通可以画一下图,很简单
			_leftSize -= sizeof(T); //表示剩余空间的大小
		}
		new(obj)T;  //定位new,因为刚申请的空间内如果是自定义类型是没有初始化的
		//所以需要可以显示的调用这个类型的构造函数,这个是专门配合内存池使用的
		return obj;
	}

	void Delete(T* obj)
	{
		obj->~T();//先把自定义类型进行析构
		//然后在进行释放,但是此时还回来的都是一块一块的小内存,无法做到一次性进行free,所以需要一个自由链表将这些小内存都挂接住
		//这里其实才是核心的关键点
		//对于指针来说,在32位的平台下面是4字节,在64位平台下面是8字节

		//头插到freeList
		//*((int*)obj)= (int)_freeList;
		Nextobj(obj) = _freeList;
		_freeList = obj;
	}
private:
	char* _memory = nullptr;//这里给char*是为了好走大小,并不是一定要给T*或者void*
	int _leftSize = 0; //为什么会加入这个成员变量呢?因为你的menory += sizeof(T),有可能就会造成越界的问题
	void* _freeList = nullptr; //给一些缺省值,让他的构造函数自己生成就可以了
};

struct TreeNode
{
	int _val;
	TreeNode* _left;
	TreeNode* _right;

	TreeNode()
		:_val(0)
		, _left(nullptr)
		, _right(nullptr)
	{}
};
void TestObjectPool()
{

	验证还回来的内存是否重复利用的问题
	ObjectPool tnPool;
	TreeNode* node1 = tnPool.New();
	TreeNode* node2 = tnPool.New();
	cout << node1 << endl;
	cout << node2 << endl;

	tnPool.Delete(node1);
	TreeNode* node3 = tnPool.New();
	cout << node3 << endl;

	cout << endl;

	//验证内存池到底快不快,有没有做到性能的优化
	//new底层本身调用的malloc,会一直和操作系统的底部打交道
	size_t begin1 = clock();
	std::vector v1;
	for (int i = 0; i < 1000000; ++i)
	{
		v1.push_back(new TreeNode);
	}
	for (int i = 0; i < 1000000; ++i)
	{
		delete v1[i];
	}
	size_t end1 = clock();


	//这里我们调用自己所写的内存池
	ObjectPool tnPool;
	size_t begin2 = clock();
	std::vector v2;
	for (int i = 0; i < 1000000; ++i)
	{
		v2.push_back(tnPool.New());
	}
	for (int i = 0; i < 1000000; ++i)
	{
		tnPool.Delete(v2[i]);
	}
	size_t end2 = clock();

	cout << end1 - begin1 << endl;
	cout << end2 - begin2 << endl;
}

C++如何实现定长内存池详解_第3张图片

这个定长的内存池依旧存在着大量的问题:

  1. 我们所采用的是取这块小内存的前4个或者8个字节来存放下一个小内存的地址,但是如果这里的模板类型T是一个char类型怎么办,它本身都没有4字节,怎么来存放?(解决的办法就是,进行一次判断如果sizeof(T) < sizeof(T*)的大小,那么就开辟T*的大小)
  2. 无法编写这个ObjectPool的析构函数,因为申请的都是一个个的小块内存,但是对于free来说,应该是一次性的对整个所开辟出来的内存块都进行释放(解决的办法就是,将这些向操作系统申请的大块内存也管理起来,如果小块内存都还回来了,那么就可以对这个大块内存进行释放)

对于上述的具体实现可以参考下面这篇文章写的很详细:

如何设计一个简单内存池

总结

到此这篇关于C++如何实现定长内存池的文章就介绍到这了,更多相关C++定长内存池内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

你可能感兴趣的:(C++如何实现定长内存池详解)