C++实现定长内存池

项目介绍

定长内存池是高并发的内存池的一部分

池化技术

池化技术 就是程序先向系统申请过量的资源 然后进行管理 以备不时之需

因为申请和释放资源都有较大的开销 不如提前申请一些资源放入 池 中 当需要资源时直接从 池 中获取 不需要时就将该资源重新放回 池 中即可 这样使用可以提高程序的运行效率 在计算机中 除了内存池之外 还有连接池 线程池 对象池等

内存池

内存池是指程序预先向操作系统申请一块足够大的内存 此后 当程序中需要申请内存时 不是直接向操作系统申请 而是直接从内存池中获取 当释放内存的时候 并不是真正将内存返回给操作系统 而是将内存返回给内存池 当程序退出时(或某个特定时间) 内存池才将之前申请的内存真正释放

内存池的作用

内存池主要解决的是效率的问题 它能够避免让程序频繁的向系统申请和释放内存 其次 内存池作为系统的内存分配器 还需要尝试解决内存碎片的问题

内部碎片和外部碎片

外部碎片是一些空闲的小块内存区域 由于这些内存空间不连续 以至于合计的内存足够 但是不能满足一些内存分配申请需求
内部碎片是由于一些对齐的需求 导致分配出去的空间中一些内存无法被使用
内存池尝试解决的是外部碎片的问题,同时也尽可能的减少内部碎片

malloc

C/C++中我们要动态申请内存并不是直接去堆申请的 而是通过malloc函数去申请 包括new也是封装了malloc函数

申请内存块时先调用malloc 再去向操作系统申请内存 malloc实际就是一个内存池

malloc的实现方式有很多种 一般不同编译器平台用的不同 Linux下的gcc用的是glibc中的ptmalloc。

定长内存池的实现

malloc就是一个通用的内存池 在什么场景下都可以使用 并不是针对某种场景专门设计的 所以malloc在什么场景下都不会有很高的性能

定长内存池就是对固定大小内存块的申请和释放的内存池 由于定长内存池只需要支持固定大小内存块的申请和释放 因此我们可以将其性能做到极致 并且在实现定长内存池时不需要考虑内存碎片等问题

定长内存池会作为高并发内存池的一个基础组件

如何实现定长

可以使用非类型模板参数,使得在该内存池中申请到的对象的大小都是N

template<size_t N>
class ObjectPool
{};

定长内存池也叫做对象池 在创建对象池时 对象池可以根据传入的对象类型的大小来实现 定长 因此我们可以通过使用模板参数来实现 定长

template<class T>
class ObjectPool
{};

如何直接向堆申请空间

在Windows下 可以调用VirtualAlloc函数 在Linux下 可以调用brk或mmap函数

#ifdef _WIN32
	#include <Windows.h>
#else   //Linux下的头文件
	    //...
#endif

//直接去堆上按页申请空间
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
	void* ptr = VirtualAlloc(0, kpage<<13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
	// linux下brk mmap等
#endif
	if (ptr == nullptr)                //申请失败抛异常
	{
		throw std::bad_alloc();
	}
	
	return ptr;
}

向堆申请到的大块内存 我们可以用一个指针来对其进行管理 还需要用一个变量来记录这块内存的长度

因为指针的类型决定了指针向前或向后走一步有多大距离 对于字符指针来说 当我们需要向后移动n个字节时 直接对字符指针进行加n操作即可

可以将这些释放回来的定长内存块链接成一个链表 为了管理释放回来的内存块的自由链表 我们还需要一个指向自由链表的指针。

因此,定长内存池当中包含三个成员变量:

_memory 指向大块内存的指针
_remainBytes 大块内存切分过程中剩余字节数
_freeList 还回来过程中链接的自由链表的头指针
内存池管理释放的对象

对于还回来的定长内存块 我们可以用链表将其链接起来 我们可以让内存块的前4个字节(32位平台)或8个字节(64位平台) 存储下一个内存块的起始地址

因此在向自由链表插入被释放的内存块时 进行链表的头插

如何让一个指针在32位平台下解引用后能向后访问4个字节 在64位平台下解引用后能向后访问8个字节

我们这里需要的是一个指向指针的指针 这里使用二级指针就行了

void*& NextObj(void* ptr)
{
	return (*(void**)ptr);
}

在释放对象时 我们应该显示调用该对象的析构函数清理该对象 如果不对其进行清理那么就可能会导致内存泄漏

释放对象

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

//将释放的对象头插到自由链表
NextObj(obj) = _freeList;
_freeList = obj;

}
当我们申请对象时 内存池应该优先把还回来的内存块对象再次重复利用 因此如果自由链表当中有内存块 就直接从自由链表头删一个内存块进行返回

如果自由链表当中没有内存块 我们就在大块内存中切出定长的内存块进行返回 当内存块切出后及时更新_memory指针的指向 以及_remainBytes的值

由于当内存块释放时我们需要将内存块链接到自由链表当中 因此我们必须保证切出来的对象至少能够存储下一个地址 所以当对象的大小小于当前所在平台指针的大小时 需要按指针的大小进行内存块的切分

当大块内存已经不足以切分出一个对象时 我们就应该调用封装的SystemAlloc函数 再次向堆申请一块内存空间 并及时更新_memory以及_remainBytes

申请对象

T* New()
{
T* obj = nullptr;

//优先使用还回来的内存块对象
if (_freeList != nullptr)
{
	//从自由链表头删一个对象
	obj = (T*)_freeList;
	_freeList = NextObj(_freeList);
}
else
{
	//保证对象能够存储得下地址
	size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
	//剩余内存不够一个对象大小时,则重新开大块空间
	if (_remainBytes < objSize)
	{
		_remainBytes = 128 * 1024;
		_memory = (char*)SystemAlloc(_remainBytes >> 13);
		if (_memory == nullptr)
		{
			throw std::bad_alloc();
		}
	}
	
	//从大块内存中切出objSize字节的内存
	obj = (T*)_memory;
	_memory += objSize;
	_remainBytes -= objSize;
}

//定位new,显示调用T的构造函数初始化
new(obj)T;

return obj;
定长内存池整体代码

//定长内存池
template
class ObjectPool
{
public:
//申请对象
T* New()
{
T* obj = nullptr;

	if (_freeList != nullptr)
	{
		//从自由链表头删一个对象
		obj = (T*)_freeList;
		_freeList = NextObj(_freeList);
	}
	else
	{
		//保证对象能够存储得下地址
		size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
		//剩余内存不够一个对象大小时,则重新开大块空间
		if (_remainBytes < objSize)
		{
			_remainBytes = 128 * 1024;
			
			_memory = (char*)SystemAlloc(_remainBytes >> 13);
			if (_memory == nullptr)
			{
				throw std::bad_alloc();
			}
		}
		//从大块内存中切出objSize字节的内存
		obj = (T*)_memory;
		_memory += objSize;
		_remainBytes -= objSize;
	}
	
	//定位new,显示调用T的构造函数初始化
	new(obj)T;

	return obj;
}
//释放对象
void Delete(T* obj)
{
	//显示调用T的析构函数清理对象
	obj->~T();

	//将释放的对象头插到自由链表
	NextObj(obj) = _freeList;
	_freeList = obj;
}

private:
char* _memory = nullptr; //指向大块内存的指针
size_t _remainBytes = 0; //大块内存在切分过程中剩余字节数

void* _freeList = nullptr;   //还回来过程中链接的自由链表的头指针

};

性能测试代码

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 = 1000000;
std::vector<TreeNode*> v1;
v1.reserve(N);

//malloc和free
size_t begin1 = clock();
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;
std::vector<TreeNode*> v2;
v2.reserve(N);
size_t begin2 = clock();
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 < N; ++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;

}
我们先用new申请若干个TreeNode对象 然后再用delete将这些对象释放掉 通过clock函数得到整个过程消耗的时间

然后将其中的new和delete替换为定长内存池当中的New和Delete 再通过clock函数得到该过程消耗的时间
在这里插入图片描述

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值