002 高并发内存池_定长内存池设计

​🌈个人主页:Fan_558
🔥 系列专栏:项目一
☀ 代码仓库:高并发内存池
🌹关注我💪🏻带你学更多操作系统知识

来自必应

前言

我们知道申请内存使用的是malloc,malloc几乎在什么场景下都可以用,意味着什么场景下都不会有很高的性能,下面我们就先来设计一个定长内存池做个开胃菜,学习它目的有两层,先熟悉一下简单内存池是如何控制的,第二它会作为我们后面内存池的一个基础组件。

一、设计整体框架

既然是内存池,那么我们首先得向系统申请一块内存空间,然后对其进行管理。要想直接向堆申请内存空间,在Windows下,可以调用VirtualAlloc函数;在Linux下,可以调用brk或mmap函数。_memory指针管理的就是我们向系统申请的一大块内存
在这里插入图片描述

1、 对于向堆申请到的大块内存,我们可以用一个指针来对其进行管理,但仅用一个指针肯 定是不够的,我们还需要用一个变量来记录这块内存的长度

size_t _SurplusBytes(剩余)

2、我们需要将这块内存进行切分,为了方便切分操作,指向这块内存的指针最好是字符指针,因为指针的类型决定了指针向前或向后走一步有多大距离,对于字符指针来说,当我们需要分配出去n个字节的空间时,直接对字符指针进行加n操作即可。

char* _memory

3、释放回来的定长内存块也需要被管理,我们可以将这些释放回来的定长内存块链接成一个链表,这里我们将管理释放回来的内存块的链表叫做自由链表,为了能找到这个自由链表,我们还需要一个指向自由链表的指针。

void* _freeList
这样我们所需的三个成员变量就定义好了
另外地,我们还需要引进一个概念:自由链表(这个结构我们在new与delete操作都会用到)

自由链表(free_List)是一种数据结构,通常用于管理可用内存块的列表。在动态内存分配中,当需要分配内存时,可以从自由链表中获取一个可用的内存块,当内存不再需要时,将其释放并加入到自由链表中。头部存储下一个节点的地址
在这里插入图片描述

整体代码框架如下:


#ifdef _WIN32
#include <Windows.h>
#else
#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;
}

template<class T>
class ObjectPool
{
public:
	//申请空间
	T* New()
	{}

	//将空间释放还回到自由链表中
	void Delete(T* obj)
	{}

private:
	char* _memory = nullptr;	// 指向内存块的指针 
	size_t  _SurplusBytes = 0;		// 内存块中剩余字节数 
	void* _freeList = nullptr;  // 管理还回来的内存对象的自由链表的头指针
};

对代码进行补充说明

既然是内存池,那么我们首先得向系统申请一块内存空间,然后对其进行管理。要想直接向堆申请内存空间,在Windows下,可以调用VirtualAlloc函数;在Linux下,可以调用brk或mmap函数。
当前环境是Windows(_WIN32定义为真),我们则使用WindowsAPI中的VirtualAlloc函数来分配内存。VirtualAlloc函数可以用来将一段虚拟地址空间分配给进程,并将其映射到物理内存上。在具体的使用上我们直接用封装过的SystemAlloc分配内存即可。

二、New操作(申请空间)

当我们申请对象的时候,内存池应该优先将释放回来的空间重复利用(自由链表中的一个个内存块),这个操作是一个头删的操作

if (_freeList)
{
	obj = (T*)_freeList;
	//指向下一个
	_freeList = *(void**)_freeList;
}

在这里插入图片描述
如果自由链表的头指针_free_List为空,则应该由堆上申请的大块内存块中分配,具体操作:让_memory加等上OBJSIZE,让surplusBytes减等上OBJSIZE即可
在这里插入图片描述
这样我们的New操作就搞定了

T* New()
{
	T* obj = nullptr; //定义指向所申请空间的指针
	//从自由链表中申请
	if (_freeList)
	{
		obj = (T*)_freeList;
		//指向下一个
		_freeList = *(void**)_freeList;
	}
	else
	{
		//扩容:如果剩余的内存块大小少于所申请的空间大小
		if (_SurplusBytes < sizeof(T))
		{
			_SurplusBytes = 128 * 1024;
			_memory = (char*)SystemAlloc(_SurplusBytes>>13);
			if (_memory == nullptr)
			{
				throw std::bad_alloc();
			}
		}
		obj = (T*)_memory;
		size_t OBJSIZE = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
		//分配空间
		_memory += OBJSIZE;		
		_SurplusBytes -= OBJSIZE;
	}
	//对开辟好空间的obj进行初始化
	//(使用定位new操作符对已经分配好的内存空间进行对象的构造(初始化)操作。当我们使用new关键字创建一个对象时,会在内存中分配一块空间来存储对象的成员变量,并调用对象的构造函数来对这块内存进行初始化。但是,在某些情况下,我们可能已经手动分配了内存空间,这时候就需要使用定位new操作符来调用对象的构造函数,以便正确地初始化这块内存空间。)
	new(obj)T;		
	return obj;
}

重点重点重点!!!

_freeList = *(void**)_freeList;
*(void**)是什么意思呢,为什么要这么写?

那就要从定长池的设计讲起了。在实现定长内存池时要做到“定长”有很多种方法,比如我们可以使用非类型模板参数,使得在该内存池中申请到的对象的大小都是N。定长内存池也叫做对象池,在创建对象池时,对象池可以根据传入的对象类型的大小来实现“定长”,因此我们可以通过使用模板参数来实现“定长”,比如创建定长内存池时传入的对象类型是int(32位下),那么该内存池就只支持4字节大小内存的申请和释放。

在这里插入图片描述

这就有一个问题了,假如在64位机器下,一个指针的大小是8字节,但是用户申请了1字节的空间,这时我们需要将空间扩为八字节再返回给用户,因为一旦这份空间被返回,1字节是无法当成指针使用存储下一个节点地址的!!!

当自由链表为空时,此时我们会从大块内存中分配空间,

当空间回收时,我们会用自由链表管理这份空间
_freeList = *(void**)_freeList;
解析:在32位下 *(void**)看的是_freeList头部四个字节,在64位下
看的是_freeList头部八个字节,这样我们就可以在每个节点头部存储下一个节点的地址,然后就可以把节点链接起来

以下这个代码是自由链表为空时,向大块内存中申请
疑问:为什么我们在大块内存中申请也是需要划定好固定大小呢?

 size_t OBJSIZE = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);

因为New操作的本质就是申请一块固定大小的空间,但是当这一块固定大小的空间被释放后,我们是需要重新拿来继续使用的(也是为了让自由链表的节点能够链接起来)
毕竟我们是不能确定机器是32位还是64位下的,当申请的空间小于32/64位下的指针,那么就开这个指针的大小,如果大于32/64位下指针的大小,那么就申请开辟空间的大小赋值给OBJSIZE

补充:当然你这里可以不用void,选择int一样可以

三、Delete操作(用自由链表管理释放的空间)

我们使用头插操作对已返还的空间做管理(当然尾插也行,只是还要找尾(ㄒoㄒ)~)

void Delete(T* obj)
{
	obj->~T();
	// 头插到freeList ,指向下一个
	*((void**)obj) = _freeList;
	_freeList = obj;
}

在这里插入图片描述

四、测试性能

比较在申请和释放大量TreeNode对象时,直接使用new和delete操作与使用对象池(ObjectPool)的效率差异

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 < 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;
}


在这里插入图片描述

小结

今日的项目分享就到这里啦,如果本文存在疏漏或错误的地方还请您能够指出!

posted @ 2024-03-24 11:32  Fan_558  阅读(12)  评论(0编辑  收藏  举报  来源