【C++】2.高并发内存池 -- 如何设计一个定长内存池

news2025/2/6 9:44:29

博客主题:如何设计一个定长内存池
个人主页:https://blog.csdn.net/sobercq
CSDN专栏:https://blog.csdn.net/sobercq/category_12884309.html
Gitee链接:https://gitee.com/yunshan-ruo/high-concurrency-memory-pool


文章目录

  • 前言
  • 定长内存池
    • 1.如何实现定长?
    • 2.分配内存
    • 3.回收内存
    • 3.如何New对象
    • 4.如何释放对象
    • 5. 优化
  • 性能测试


前言

上期我们说到malloc,我们知道malloc,在C/C++中动态申请内存都是通过malloc去申请内存, 并且malloc就是一个内存池。但是malloc因为要兼容通用,它什么场景下都可以用,但是什么场景下都可以用就意味着什么场景下都不会有很高的性能,所以我们要设计一个针对点一定场景下的内存池,来实现高性能。
在这里插入图片描述
所以本期我们就先设计一个定长内存池,先熟悉一下简单内存池是如何控制的,第二他会作为我们后面高并发内存池的一个基础组件。

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

定长内存池

1.如何实现定长?

我们先创建和Test.cpp,ObjectPool.h,那如何实现定长,其一我们可以采用非类型模板参数来实现定长。

template<size_t N>

class ObjectPool
{
};

其二呢,我们也可以通过类型模板参数,采用这种方法设计我们也把它叫做对象池,即ObjectPool,在创建对象池时,对象池可以根据传入的对象类型的大小来实现“定长”,比如,创建定长内存池时传入的对象类型是内置类型,那么该内存池就只支持内置类型字节大小的内存块的申请和释放,如果是自定义类型,那我们的内存块大小就是sizeof(T)。

template<class T>

class ObjectPool
{
};

2.分配内存

首先,我们的内存池需要去堆上申请一块内存,这块内存假设我们已经申请下来了,我们要管理这块内存。那如何分配这大块内存呢?
如图所示,我们每次只需要在头部位置加对应大小的字节就可以了。
在这里插入图片描述
所以为了方便管理我们申请下来的大块内存,就需要用一个指针来管理。

//void* _memory
char* _memory

但是void这个类型没有意义,又不能加也不能减,还要强转,所以我们取最小的char,方便我们去切内存块,char指针+1也是+1字节。容易切分内存。

3.回收内存

那我们使用完这些划分后的内存块后,应该如何管理呢?我们可以用一个自由链表来管理内存,将回收来的内存块想象成一个链表的结点。
在这里插入图片描述
通过上图,我们能明确知道,这个对象池的构造了。

template <class T>

class ObjectPool
{
private:
	char* _memory = nullptr;	//管理系统申请的大块内存
	void* _freelist = nullptr; //管理回收内存
};

3.如何New对象

我们既然已经知道我们的内存池的构造了,接下来就是如何去new一个对象。
刚才我们也说过,对象池,其大小是一个T的大小,所以我们可以先通过malloc去申请一块大点的空间,然后将其分配给对象。
在这里插入图片描述

T* New()
{
	T* Obj = nullptr;
	if (_memory == nullptr)
	{
		_memory = (char*)malloc(128 * 1024);//128K内存块
		if (_memory == nullptr)
		{
			throw std::bad_alloc();//申请失败,抛异常
		}
	}

	Obj = (T*)_memory;
	_memory += sizeof(T);
	return Obj;
}

我们创建一个T*的对象出来,然后通过malloc申请一块空间,申请失败就抛异常,成功我们就赋值给对象,然后_memory就加上对应大小的字节数。
但是,我们怎么知道我们malloc申请下来的空间数都用完了呢?
所以我们可以再创建一个成员变量_remainBytes来监视我们的内存块,如果我们的剩余字节数无法再分配出一个内存块的话,就要再通过malloc去申请一个内存块。

T* New()
{
	T* Obj = nullptr;
	//剩余字节数不够分配,再创建一个空间
	if (_remainBytes < sizeof(T))
	{
		_memory = (char*)malloc(128 * 1024);//128K内存块
		if (_memory == nullptr)
		{
			throw std::bad_alloc();//申请失败,抛异常
		}
		_remainBytes = 128 * 1024;
	}

	//直接分配内存
	Obj = (T*)_memory;
	_remainBytes -= sizeof(T);
	_memory += sizeof(T);

	return Obj;
}

4.如何释放对象

第一次回收内存,我们只要让对象的头4字节或者8字节为空。而32位平台下指针的大小是4个字节,64位平台下指针的大小是8个字节。而指针指向数据的类型,决定了指针解引用后能向后访问的空间大小,因此我们这里需要的是一个指向指针的指针,这里使用二级指针就行了。
在这里插入图片描述

void Delete(T* obj)
{
	_freelist = obj;
	//令对象的指针区为空
	*(void**)obj = nullptr;
}

回收对象并不只有第一次,也有很多次,回收对象并不只有第一次,所以多次以后我们就要多批处理。因为是内存块要回收到自由链表当中,我们可以选择头插和尾插,以前学习链表我们就学过要,尾插要找尾,更麻烦,而头插就方便更多。
所以我们这里就和链表一样直接头插。

void Delete(T* obj)
{
    if(_freelist == nullptr)
    {
        //第一次插入
        _freelist = obj;
        //令对象的指针区为空
        *(void**)obj = nullptr;
    }
    else
    {
        //其余插入
        *(void**)obj = _freelist;
        _freelist = obj;
	}
}

那这段代码还有没有优化空间呢?我们可以发现头插其实根本不用分第一次和n次,因为不影响我们的操作。
所以我们可以直接去掉if else

void Delete(T* Obj)
{
	//令对象的指针区为空
	*(void**)Obj = _freelist;
	_freelist = Obj;
}

回收后我们还需要重复利用我们自由链表上的内存,所以在划分内存前,我们需要先检查一步,我们的自由链表是否为空。不为空,我们直接把自由链表中的内存块拿出来使用。

//重复利用自由链表
if (_freelist != nullptr)
{
	//自由链表头删
	void* next = *((void**)_freelist);
	Obj = (T*)_freelist;
	_freelist = next;
	return Obj;
}
else
{
	//....
}

5. 优化

但,根据上述情况我们确实写完了大部分,但可能存在一种情况,我自由链表回收内存的时候无法开辟字节空间怎么办?也就是sizeof(T) < * (void **) 的情况。所以为了保证我们自由链表能有4/8的字节空间,我们还需要在分配内存的时候保证一个最低限度。

//直接分配内存
Obj = (T*)_memory;
size_t ObjSize = sizeof(T) > sizeof(void*) ? sizeof(T) : sizeof(void*);
_remainBytes -= ObjSize;
_memory += ObjSize;

其次我们还需要处理一下对象。在释放对象时,应该显示调用该对象的析构函数清理该对象,因为该对象可能还管理着其他某些资源,如果不对其进行清理那么这些资源将无法被释放,就会导致内存泄漏。
同理,当内存块切分出来后,我们也应该使用定位new,显示调用该对象的构造函数对其进行初始化。

//定位new
new(Obj)T;
return Obj;
		
void Delete(T* Obj)
{
	//清理对象资源
	Obj->~T();
	//令对象的指针区为空
	*(void**)Obj = _freelist;
	_freelist = Obj;
}

3.我们能否跳过malloc直接去堆上申请内存呢?答案是可以的,就是用接口去使用,VirtualAlloc(按页去申请),Linux可以调用brk或mmap函数。
在使用前我们得包括对应的头文件。
我们可以为了通用在前面先使用上条件编译,从而让我们的内存池在Linux和Windows下都可以做到使用。

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

对应的,我们需要修改一下部分代码,将malloc去除。

//剩余字节数不够分配,再创建一个空间
if (_remainBytes < sizeof(T))
{
	_remainBytes = 128 * 1024;
	//_memory = (char*)malloc(128 * 1024);//128K内存块
	_memory = (char*)SystemAlloc(_remainBytes);
	if (_memory == nullptr)
	{
		throw std::bad_alloc();//申请失败,抛异常
	}
}

这样我们就可以直接从堆上申请空间了。那这里我们到底搞了多少内存呢?
首先,这些是我们的计算机单位,23就1B,210就1024B = 1KB,而VirtualAlloc中kpage << 13,就相当于213 * kpage, 即 4096 * kpage。
所以实际上这里给到的内存就有128 * 1024 * 4096 bytes= 512 * 1024 *1024 = 512MB。
当然这里会过大,所以我们可以调整一下。
我自己测试在visual 2022 x64情况下可以申请到 128 * 1024 * 32 * 1024 = 4gb。
x86情况下则是128mb。
我没有很仔细去分配,只是大概的测试了一下,所以各位友友如果有问题不妨降低一下申请的字节数。

性能测试

这里是一个测试性能用的代码段。
new 和 delete 测试:使用 std::vector<TreeNode*> 容器存储通过 new 分配的 TreeNode 对象。在每轮测试结束后,通过 delete 逐一释放对象,并清空 v1 容器。
对象池(ObjectPool)测试:创建一个 ObjectPool 对象池,使用对象池的 New() 方法分配对象,用 Delete() 方法释放对象。同样在每轮测试结束后,清空 v2 容器。


void TestObjectPool()
{
	// 申请释放的轮次
	const size_t Rounds = 3;
	// 每轮申请释放多少次
	const size_t N = 100000;

	//new和delete
	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;
}

在这里插入图片描述
通过测试还是明显的看出我们的内存池速度会更快些,当然这是debug的情况下, 如果我们切换到release,可以更明显看到差别。
在这里插入图片描述


结尾:
完整代码在我的gitee仓库中,可以点击文章开头的链接跳转,关于定长内存池我们就结束了。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Redis --- 使用Feed流实现社交平台的新闻流

要实现一个 Feed 流&#xff08;类似于社交媒体中的新闻流&#xff09;&#xff0c;通常涉及以下几个要素&#xff1a; 内容发布&#xff1a;用户发布内容&#xff08;例如文章、状态更新、图片等&#xff09;。内容订阅&#xff1a;用户可以订阅其他用户的内容&#xff0c;获…

游戏引擎学习第88天

仓库:https://gitee.com/mrxiao_com/2d_game_2 调查碰撞检测器中的可能错误 在今天的目标是解决一个可能存在的碰撞检测器中的错误。之前有人提到在检测器中可能有一个拼写错误&#xff0c;具体来说是在测试某个变量时&#xff0c;由于引入了一个新的变量而没有正确地使用它&…

c++中priority_queue的应用及模拟实现

1.介绍 priority_queue 是一种数据结构&#xff0c;它允许你以特定的顺序存储和访问元素。在 C 标准模板库&#xff08;STL&#xff09;中&#xff0c;priority_queue 是一个基于容器适配器的类模板&#xff0c;它默认使用 std::vector 作为底层容器&#xff0c;并且默认使用最…

游戏引擎 Unity - Unity 设置为简体中文、Unity 创建项目

Unity Unity 首次发布于 2005 年&#xff0c;属于 Unity Technologies Unity 使用的开发技术有&#xff1a;C# Unity 的适用平台&#xff1a;PC、主机、移动设备、VR / AR、Web 等 Unity 的适用领域&#xff1a;开发中等画质中小型项目 Unity 适合初学者或需要快速上手的开…

【Elasticsearch】geohex grid聚合

在 Elasticsearch 中&#xff0c;地理边界过滤是一种用于筛选地理数据的技术&#xff0c;它可以根据指定的地理边界形状&#xff08;如矩形、多边形等&#xff09;来过滤符合条件的文档。这种方法在地理空间数据分析中非常有用&#xff0c;尤其是在需要将数据限制在特定地理区域…

crewai框架第三方API使用官方RAG工具(pdf,csv,json)

最近在研究调用官方的工具&#xff0c;但官方文档的说明是在是太少了&#xff0c;后来在一个视频里看到了如何配置&#xff0c;记录一下 以PDF RAG Search工具举例&#xff0c;官方文档对于自定义模型的说明如下&#xff1a; 默认情况下&#xff0c;该工具使用 OpenAI 进行嵌…

算法 哈夫曼树和哈夫曼编码

目录 前言 一&#xff0c;二进制转码 二&#xff0c;哈夫曼编码和哈夫曼树 三&#xff0c;蓝桥杯 16 哈夫曼树 总结 前言 这个文章需要有一定的树的基础&#xff0c;没学过树的伙伴可以去看我博客树的文章 当我们要编码一个字符串转成二进制的时候&#xff0c;我们要怎么…

Sumatra PDF:小巧免费,满足多样阅读需求

Sumatra PDF是一款完全免费的本地阅读器软件&#xff0c;以小巧的体积和全面的功能受到用户青睐。如今&#xff0c;它已经更新到3.3版本&#xff0c;带来了更多实用功能&#xff0c;尤其是新增的注释功能&#xff0c;值得我们再次关注。 软件特色 轻量级体积&#xff1a;压缩…

TiDB 分布式数据库多业务资源隔离应用实践

导读 随着 TiDB 在各行业客户中的广泛应用 &#xff0c;特别是在多个业务融合到一套 TiDB 集群中的场景&#xff0c;各企业对集群内多业务隔离的需求日益增加。与此同时&#xff0c;TiDB 在多业务融合场景下的资源隔离方案日趋完善&#xff0c;详情可参考文章 《你需要什么样的…

105,【5】buuctf web [BJDCTF2020]Easy MD5

进入靶场 先输入试试回显 输入的值成了password的内容 查看源码&#xff0c;尝试得到信息 什么也没得到 抓包&#xff0c;看看请求与响应里有什么信息 响应里得到信息 hint: select * from admin where passwordmd5($pass,true) 此时需要绕过MD5&#xff08;&#xff09;函…

BFS(广度优先搜索)——搜索算法

BFS&#xff0c;也就是广度&#xff08;宽度&#xff09;优先搜索&#xff0c;二叉树的层序遍历就是一个BFS的过程。而前、中、后序遍历则是DFS&#xff08;深度优先搜索&#xff09;。从字面意思也很好理解&#xff0c;DFS就是一条路走到黑&#xff0c;BFS则是一层一层地展开。…

33.Word:国家中长期人才发展规划纲要【33】

目录 NO1.2样式​ NO3​ 图表 ​ NO4.5.6​ 开始→段落标记视图→导航窗格→检查有无遗漏 NO1.2样式 F12/另存为&#xff1a;Word.docx&#xff1a;考生文件夹样式的复制样式的修改 样式的应用&#xff08;没有相似/超级多的情况下&#xff09;——替换 [ ]通配符&#x…

gym-anytrading

参考&#xff1a;https://github.com/upb-lea/gym-electric-motor AnyTrading 是一组基于 reinforcement learning (RL) 的 trading algorithms&#xff08;交易算法&#xff09;的 OpenAI Gym 环境集合。 该项目主要用于foreign exchange (FOREX) 和 stock markets (股票市场)…

如何自定义软件安装路径及Scoop包管理器使用全攻略

如何自定义软件安装路径及Scoop包管理器使用全攻略 一、为什么无法通过WingetUI自定义安装路径&#xff1f; 问题背景&#xff1a; WingetUI是Windows包管理器Winget的图形化工具&#xff0c;但无法直接修改软件的默认安装路径。原因如下&#xff1a; Winget设计限制&#xf…

私有化部署 DeepSeek + Dify,构建你的专属私人 AI 助手

私有化部署 DeepSeek Dify&#xff0c;构建你的专属私人 AI 助手 概述 DeepSeek 是一款开创性的开源大语言模型&#xff0c;凭借其先进的算法架构和反思链能力&#xff0c;为 AI 对话交互带来了革新性的体验。通过私有化部署&#xff0c;你可以充分掌控数据安全和使用安全。…

Java 进阶 01 —— 5 分钟回顾一下 Java 基础知识

Java 进阶 01 —— 5 分钟回顾一下 Java 基础知识 Java 生态圈Java 跨平台的语言 Java 虚拟机规范JVM 跨语言的平台多语言混合编程两种架构 举例 JVM 的生命周期 虚拟机的启动虚拟机的执行虚拟机的退出 JVM 发展历程 Sun Classic VMExact VMHotSpotBEA 的 JRockitIBM 的 J9 …

V103开发笔记1-20250113

2025-01-13 一、应用方向分析 应用项目&#xff1a; PCBFLY无人机项目&#xff08;包括飞控和手持遥控器&#xff09;&#xff1b; 分析移植项目&#xff0c;应用外设资源包括&#xff1a; GPIO, PWM,USART,GPIO模拟I2C/SPI, ADC,DMA,USB等&#xff1b; 二、移植项目的基本…

DeepSeek研究员在线爆料:R1训练仅用两到三周,春节期间观察到R1 zero强大进化

内容提要 刚刚我注意到DeepSeek研究员Daya Guo回复了网友有关DeepSeek R1的一些问题&#xff0c;以及接下来的公司的计划&#xff0c;只能说DeepSeek的R1仅仅只是开始&#xff0c;内部研究还在快速推进&#xff0c;DeepSeek 的研究员过年都没歇&#xff0c;一直在爆肝推进研究…

LLM推理--vLLM解读

主要参考&#xff1a; vLLM核心技术PagedAttention原理 总结一下 vLLM 的要点&#xff1a; Transformer decoder 结构推理时需要一个token一个token生成&#xff0c;且每个token需要跟前序所有内容做注意力计算&#xff08;包括输入的prompt和该token之前生成的token&#xf…

vscode软件操作界面UI布局@各个功能区域划分及其名称称呼

文章目录 abstract检查用户界面的主要区域官方文档关于UI的介绍 abstract 检查 Visual Studio Code 用户界面 - Training | Microsoft Learn 本质上&#xff0c;Visual Studio Code 是一个代码编辑器&#xff0c;其用户界面和布局与许多其他代码编辑器相似。 界面左侧是用于访…