【高并发内存池】基本框架 + 固定长度内存池实现 1

news2024/9/23 23:03:37

高并发内存池

  • 1. 基本框架
  • 2. 定长内存池的实现
    • 2.1 介绍定长内存池
    • 2.2 T* New()
    • 2.3 void Delete(T* obj)
  • 3. 源码(附赠测试)
  • 4. 总结

1. 基本框架

高并发内存池主要由三个部分构成:
1.thread cache:用于小于256KB的内存的分配。线程缓存是每个线程独有的,线程从这⾥申请内存不需要加锁,每个线程独享⼀个cache,这也就是这个并发线程池⾼效的地⽅。

2.cetral cache:中心缓存是所有线程所共享,thread cache是按需从central cache中获取的对象。central cache合适的时机回收thread cache中的对象,避免⼀个线程占⽤了太多的内存,⽽其他线程的内存吃紧,达到内存分配在多个线程中更均衡的按需调度的⽬的。

central cache是存在竞争的,所以从这⾥取内存对象是需要加锁,⾸先这里用的是桶锁,其次只有thread cache的没有内存对象时才会找central cache,所以这⾥竞争不会很激烈。

3.page cache : ⻚缓存是在central cache缓存上⾯的⼀层缓存,存储的内存是以⻚为单位存储及分配的,central cache没有内存对象时,从page cache分配出⼀定数量的page,并切割成定长大小的小块内存,分配给central cache。

当⼀个span的⼏个跨度⻚的对象都回收以后,page cache会回收central cache满⾜条件的span对象,并且合并相邻的⻚,组成更⼤的⻚,缓解内存碎片的问题。
在这里插入图片描述

2. 定长内存池的实现

实现一个定长内存池,主要完成的功能就是两个:申请和释放。

2.1 介绍定长内存池

在这里插入图片描述
从图中可以看到_memory是我们向系统申请的一大块内存。
_freeList表示的是自由链表。

这里需要说明一下,为什么会使用自由链表:因为我们写的内存池,所以,申请得到的内存不是用完就立马还回去,而是用完之后用一个自由链表链接起来,这样下次再申请内存的时候就不用在堆里面申请了,而是直接在自由链表里面获取,大大的提高了效率。

定长内存池使用的流程
1.申请内存的时候,判断系统中是否有_freeList,如果有,判断_freeList的大小是否大于等于申请的内存,如果是,就直接从_freeList中拿。这些都不满足就去堆里申请了。

2.还回来内存的时候,直接将还回来的内存链入到_freeList的后面。

框架如下:

template<class T>
class ObjectPool
{
public:
	T* New()
	{

	}

	void Delete(T* obj)
	{

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

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

2.2 T* New()

首先理解自由链表的结构:
在这里插入图片描述
每一个_freeList前面的部分存下一个自由链表的地址,这样就每个_freeList就链接起来了。

处理T* New()的逻辑:当系统要申请一块内存,我们设定为T* obj = nullptr;
此时需要判断是在堆上申请还是在从_freeList中拿。
如果_freeList不为空,就代表可以从_freeList中拿。

下面处理从_freeList中拿内存的逻辑:
可以理解为从_freeList中头删。

思考在单链表中是如何头删的?先保存下一个节点,然后让头节点指向下一个节点。

在这里插入图片描述

这里涉及一个问题:next如何取?
为什么说这是一个问题,因为我们不知道所要取用的大小,如果是char类型,就是1;如果是int类型,就是4;如果是double类型,就是8;这里要取用的是next,是一个地址,大小是指针大小。我们不知道指针大小是多少(这根据不同的平台会有不同)。

那么,我们该如何取一个指针的大小呢?
int*,存的是int的地址,int存的是int*的地址。(int)解引用就是int*的大小。

因此,我们可以使用 * (void**)表示的就是void*类型,而*((void**)_freeList)就表示取_freeList前指针大小个字节,也就是地址。

第二种情况,就是没有还回来的内存,没有_freeList,该怎么办呢?
在这里插入图片描述
这里没什么好说的。

obj取到了T大小的空间,还需要判断一下,这个T大小的空间是否比void*大,如果比void*小,要给void*大小的内存。这是因为我们至少要保证取下来的内存能存一个地址,不然无法链接回来了。
在这里插入图片描述

最终代码如下:

	T* New()
	{
		T* obj = nullptr;
		//1.优先把换回来的内存块再次重复利用
		if (_freeList)  //代表有还回来的内存块对象
		{
			//重复利用,进行单链表的头删 
			//这里的*(void**)_freeList表示取freeLiset的前指针大小个字节 -->也就是取next的地址
			void* next = *((void**)_freeList);     //?  这里为什么使用void**

			//obj是取整个对象
			obj = (T*)_freeList;
			//往后移动一位
			_freeList = next;
		}
		else  //如果没有还回来的内存块
		{
			//判断开辟的一大块空间是否够这次申请的
			if (_remainBytes < sizeof(T))  
			{
				//如果不够
				_remainBytes = 1024 * 128;   //申请128KB
				//这里为什么要这么写?
				_memory = (char*)malloc(_remainBytes);
				//SystemAlloc是系统调用接口
				//_memory = (char*)SystemAlloc(_remainBytes >> 13);
				
				//异常处理
				if (_memory == nullptr)
				{
					throw std::bad_alloc();
				}
			}
			//创建一个对象指向memory,这个obj也就是我们申请内存给到的实体
			obj = (T*)_memory;

			//这里判断的意义是:如果我们申请的空间大于指针的大小,就返回我们申请的空间,
			//否则返回指针的大小  --> 这样做是为了内存块至少能存下地址
			size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);

			//大块内存的指针向后移动
			_memory += objSize;
			//可用空间减少
			_remainBytes -= objSize;
		}

		new(obj) T;

		return obj;
	}

2.3 void Delete(T* obj)

这个过程可以看成是对_freeList的头插。
在这里插入图片描述

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

		//头插
		//将obj内存块的前指针大小的字节存入_freeList的地址
		*(void**)obj = _freeList;
		//将_freeList移动为头结点
		_freeList = obj;
	}

3. 源码(附赠测试)

#pragma once
#include"Common.h"


template<class T>
class ObjectPool
{
public:
	//申请内存
	T* New()
	{
		T* obj = nullptr;
		//1.优先把换回来的内存块再次重复利用
		if (_freeList)  //代表有还回来的内存块对象
		{
			//重复利用,进行单链表的头删 
			//这里的*(void**)_freeList表示取freeLiset的前指针大小个字节 -->也就是取next的地址
			void* next = *((void**)_freeList);     //?  这里为什么使用void**

			//obj是取整个对象
			obj = (T*)_freeList;
			//往后移动一位
			_freeList = next;
		}
		else  //如果没有还回来的内存块
		{
			//判断开辟的一大块空间是否够这次申请的
			if (_remainBytes < sizeof(T))  
			{
				//如果不够
				_remainBytes = 1024 * 128;   //申请128KB
				//这里为什么要这么写?
				_memory = (char*)malloc(_remainBytes);
				//SystemAlloc是系统调用接口
				//_memory = (char*)SystemAlloc(_remainBytes >> 13);
				
				//异常处理
				if (_memory == nullptr)
				{
					throw std::bad_alloc();
				}
			}
			//创建一个对象指向memory,这个obj也就是我们申请内存给到的实体
			obj = (T*)_memory;

			//这里判断的意义是:如果我们申请的空间大于指针的大小,就返回我们申请的空间,
			//否则返回指针的大小  --> 这样做是为了内存块至少能存下地址
			size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);

			//大块内存的指针向后移动
			_memory += objSize;
			//可用空间减少
			_remainBytes -= objSize;
		}

		new(obj) T;

		return obj;
	}

	//这是释放的过程,将释放的小内存块挂到_freeList前面
	void Delete(T* obj)
	{
		//显式调用函数清理对象
		obj->~T();

		//头插
		//将obj内存块的前指针大小的字节存入_freeList的地址
		*(void**)obj = _freeList;
		//将_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 Test1()
{
	// 申请释放的轮次
	const size_t Rounds = 5;

	// 每轮申请释放多少次
	const size_t N = 100000;

	std::vector<TreeNode*> v1;
	v1.reserve(N);

	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();

	std::vector<TreeNode*> v2;
	v2.reserve(N);

	ObjectPool<TreeNode> TNPool;
	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;
}

4. 总结

本文主要讲了两个方面的内容,分别是高并发内存池的结构和实现定长内存池。

  1. 实现定长内存池的思路:
    1.1 从系统中申请T大小的内存obj,首先要判断系统中是否有_freeList,如果有,直接将_freeList头删,将切出来的节点给obj.
    1.2 如果没有_freeList,就直接从之前申请的大内存_memory中申请,如果_memory的大小不够申请的T的大小,就从堆上申请。如果_memory够大,就从_memory中切一个大小为T的内存块给obj.

2.如何取_freeList的前指针大小个节点。*(void**) obj = _freeList这句话就等价于obj->next = _freeList

*(void**) next = _freeList就表示取_freeList 的前指针大小个字节,而_freeList的前指针大小个字节存的是下一个内存块的地址,因此这句话表示取_freeList的下一个内存块,等价于next = _freeList->next.
在这里插入图片描述

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

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

相关文章

解决element plus报错ResizeObserver loop completed with undelivered notifications.

1、问题描述 在使用动态数据切换渲染el-table表格过程中&#xff0c;报错如下&#xff1a; ResizeObserver loop completed with undelivered notifications. 2、解决方案 在网上找了很多办法&#xff0c;包括&#xff1a; 为每一列指定宽度&#xff0c;试了&#xff0c;问题…

【AcWing】基础算法

目录 1、快速排序 1.1 快速排序 1.2 第k个数 2、归并排序 2.1 归并排序 2.2 逆序对的数量 3、二分 3.1 数的范围 3.2 数的三次方根 4、高精度 4.1 高精度加法 4.2 高精度减法 4.3 高精度乘法 4.4 高精度除法 5、前缀和与差分 5.1 前缀和 5.2 子矩阵的和 5.3 …

0.设计模式总览——设计模式入门系列

在现代软件开发中&#xff0c;设计模式为我们提供了优秀的解决方案&#xff0c;帮助我们更好地组织代码和架构。本系列专栏将对设计模式的基本思想、原则&#xff0c;以及常用的分类、实现方式&#xff0c;案例对比、以及使用建议&#xff0c;旨在提高开发者对设计模式的理解和…

数据库的学习

第一章 绪论 基本概念 数据---描述事物的符号 数据库DB---长期存在在计算机内、有组织的、可共享的 大量数据的集合 数据库管理系统DBMS---位于用户和操作系统之间的一层数据管理软件 数据库系统DBS---由数据库、数据库管理系统&#xff08;及其应用开发工具&#xff09;、…

VBA技术资料MF201:添加简单的右键菜单

我给VBA的定义&#xff1a;VBA是个人小型自动化处理的有效工具。利用好了&#xff0c;可以大大提高自己的工作效率&#xff0c;而且可以提高数据的准确度。“VBA语言専攻”提供的教程一共九套&#xff0c;分为初级、中级、高级三大部分&#xff0c;教程是对VBA的系统讲解&#…

Linux bash特性:

命令别名&#xff1a; 命令行定义命令别名&#xff1a; alias cdt‘cd’ alias 别名‘完整命令’ unlias----删除别名 但是断开连接后&#xff0c;再次链接&#xff0c;命令别名会失效&#xff0c;不同窗口别名也不生效 配置文件内更改命令别名&#xff1a; 在对应用户的…

导入时,Excel模板不被下载

问题描述 提示&#xff1a;这里描述项目中遇到的问题&#xff1a; 这是个SSM项目&#xff0c;以前经常遇到这个问题&#xff0c;今天有幸记录下来 [ERROR][o.a.s.r.StreamResult] Can not find a java.io.InputStream with the name [downLoadFile] in the invocation stack…

【计算机网络篇】计算机网络概述

本文主要介绍计算机网络第一章节的内容&#xff0c;文中的内容是我认为的重点内容&#xff0c;并非所有。参考的教材是谢希仁老师编著的《计算机网络》第8版。跟学视频课为河南科技大学郑瑞娟老师所讲计网。 文章目录 &#x1f3af;一.计算机网络的组成 ✨主要内容 1.边缘部…

操作系统笔记三

进程 把一个静态程序通过OS在内存中让cpu执行起来的动态执行过程叫进程 写代码都是用户态&#xff0c;而进程在执行过程中需要完成特定的功能&#xff0c;这些功能呢只有操作系统能提供&#xff0c;比如说读写文件&#xff0c;读写文件的过程是与硬盘打交道&#xff0c;这个过程…

106.游戏安全项目-机制插件分析技巧-指针扫描

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 内容参考于&#xff1a;易道云信息技术研究院 本人写的内容纯属胡编乱造&#xff0c;全都是合成造假&#xff0c;仅仅只是为了娱乐&#xff0c;请不要盲目相信…

【初阶数据结构】详解二叉树 - 树和二叉树(三)(递归的魅力时刻)

文章目录 前言1. 二叉树链式结构的意义2. 手搓一棵二叉树3. 二叉树的遍历&#xff08;重要&#xff09;3.1 遍历的规则3.2 先序遍历3.3 中序遍历3.4 后序遍历3.5 遍历的代码实现3.5.1 先序遍历代码实现3.5.2 中序遍历代码实现3.5.3 后序遍历代码实现 4. 统计二叉树结点的个数5.…

Python 函数用法与底层分析

在编写函数时&#xff0c;函数体中的代码写法和我们前面讲述的基本一致&#xff0c;只是对代码实现了封装&#xff0c;并增加了函数调用、传递参数、返回计算结果等内容。 函数简介函数(function)的基本概念 1&#xff1a;一个程序由一个一个的任务组成&#xff1b;函数就是代…

《CUDA编程》1.GPU硬件与CUDA环境搭建

1 GPU 介绍 GPU&#xff08;graphics processing unit&#xff09;&#xff0c;意为图形处理器&#xff0c;也被称为显卡&#xff08;graphics card&#xff09;。GPU的浮点数运算峰值就比同时期的CPU高一个量级&#xff1b;GPU的内存带宽峰值也比同时期的CPU高一个量级。 CP…

【重学 MySQL】三十一、字符串函数

【重学 MySQL】三十一、字符串函数 函数名称用法描述ASCII(S)返回字符串S中的第一个字符的ASCII码值CHAR_LENGTH(s)返回字符串s的字符数&#xff0c;与CHARACTER_LENGTH(s)相同LENGTH(s)返回字符串s的字节数&#xff0c;和字符集有关CONCAT(s1,s2,…,sn)连接s1,s2,…,sn为一个字…

数据加密和数字证书

1 什么是数据加密 数据加密的基本过程就是对原来为明文的文件或数据按某种算法进行处理,使其成为不可读的一段代码,通常称为"密文",使其只能在输入相应的密钥之后才能显示出本来内容,通过这样的途径来达到保护数据不被非法人窃取、阅读的目的。 该过程的逆过程…

【开源免费】基于SpringBoot+Vue.JS服装商城系统(JAVA毕业设计)

本文项目编号 T 046 &#xff0c;文末自助获取源码 \color{red}{T046&#xff0c;文末自助获取源码} T046&#xff0c;文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析 六、核心代码6.1 新…

数据结构篇--折半查找【详解】

折半查找也叫做二分查找或者对数查找&#xff0c;是一种在有序数组中查找特定元素的查找算法。 折半查找的算法步骤如下&#xff1a; 将目标关键字key与数组中的中间元素比较&#xff0c;若相等则查找成功。key大于中间元素&#xff0c;就到数组中大于中间元素的部分进行查找&…

c++语法(模板初阶+类和对象部分知识点)

1:泛型编程 2:模板 2.1:函数模板 2.2:类模板 3:const成员函数与非const的区别 4:构造函数之初始化列表 4.1:初始化列表语法及其应用 4.2:explicit关键字 5:static成员变量,static成员函数 1:泛型编程 首先在提出泛型编程前我们先来看一下的代码(关于swap函数)。 void swap…

FPGA随记——VIVADO中ASYNC_REG指令

参考文章&#xff1a;Vivado综合属性系列一、ASYNC_REG_asyncregtrue-CSDN博客 -很棒棒的 跨时钟域设计&#xff08;CDC&#xff09;是个老生常谈的问题&#xff0c;其场景很多很杂&#xff0c;其中一个比较为人熟知的就是单bit信号从慢时钟到快时钟所采用的两级寄存器处理的…

抖音矩阵系统源码搭建短视频批量剪辑矩阵分发,可开源或oem

打造多语言短视频平台&#xff1a;技术实施方案揭秘 在短视频矩阵系统技术开发实施方案中&#xff0c;数据库设计是基础环节。首先&#xff0c;需要建立语言包数据库表并填充初始文本数据&#xff0c;如英语和中文的常用语。接着&#xff0c;要设计高效的数据库连接和数据访问接…