四、高并发内存池整体框架设计

四、高并发内存池整体框架设计

现代很多的开发环境都是多核多线程,在申请内存的场景下,必然存在激烈的锁竞争问题。malloc本身其实已经很优秀,那么我们项目的原型TCmalloc就是在多线程高并发的场景下更胜一筹,所以这次我们实现的内存池需要考虑以下几方面的问题。

  1. 性能问题。
  2. 多线程环境下,锁竞争问题。
  3. 内存碎片问题。

concurrent memory pool主要由以下3个部分构成:
1、thread cache:线程缓存是每个线程独有的,用于分配小于256KB的内存的,线程从这里申请内存不需要加锁,每个线程独享一个thread cache,这也就是这个并发线程池高效的地方。
2、central 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对象,并且合并相邻的页,组成更大的页,缓解内存碎片的问题,当central cache再一次向PageCache申请更大块内存的时候PageCache也能满足。

在这里插入图片描述

内存池三层模型中函数需要用到的公共的头文件–common.h


//能够申请的内存的最大值
static const size_t MAX_BYTES = 256 * 1024;
//thread_cache的哈希桶的数目
static const size_t NFREELIST = 208;
//PageCache中哈希桶的数量,每一个桶按照页数映射
//每一个页数对应的桶的span对象的大小就是页数,即第一页的span对象的大小是一页
static const size_t NPAGES = 129;
//页大小转换偏移, 即一页定义为2^13,也就是8KB
static const size_t PAGE_SHIFT = 13;

//条件编译,为了适应不同平台
#ifdef _WIN64
typedef unsigned long long PAGE_ID;
#elif _WIN32
typedef size_t PAGE_ID;//页号
#endif

#ifdef _WIN32
#include <windows.h>
#endif // _WIN32
//系统调用接口,直接向系统堆申请k页内存,脱离malloc函数
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
	void* ptr = VirtualAlloc(0, kpage * (1 << PAGE_SHIFT),
		MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
	// brk mmap等
#endif
	if (ptr == nullptr)
		throw std::bad_alloc();
	return ptr;
}
//系统调用接口,直接向系统堆释放内存,脱离free
inline static void SystemFree(void* ptr)
{
#ifdef _WIN32
	VirtualFree(ptr, 0, MEM_RELEASE);
#else
	// sbrk unmmap等
#endif
}


//obj对象的前4个或者8个字节,存放的是obj的下一个对象的地址,相当于obj->next
static void*& NextObj(void* obj)
{
	return *(void**)obj;
}


//管理切分好的小对象的自由链表
struct FreeList
{
public:
	void Push(void* obj)
	{
		assert(obj);
		//头插
		NextObj(obj) = _freeList;
		_freeList = obj;

		_size++;
	}

	void PushRange(void* start,void* end,size_t n)
	{
		assert(start);
		assert(end);

		NextObj(end) = _freeList;
		_freeList = start;

		调试技巧:
		1、条件断点
		2、调用堆栈
		3、中断程序
		//int i = 0;
		//while (start)
		//{
		//	start = NextObj(start);
		//	i++;
		//}
		//if (i != n)
		//{
		//	int x = 0;
		//}

		_size += n;
	}

	//n表示删除了多少个,这里的删除并不是真的把空间释放了,而是把小对象从自由链表中解掉
	//即可,删除掉的这些小对象本质也是被外面调用方函数拿到了的,注意这里start和end是引用
	//输出型参数
	void PopRange(void*& start, void*& end, size_t n)
	{
		assert(n <= _size);

		start = _freeList;
		end = start;

		//end走n-1步是为了修改end的next和更新_freeList
		for (size_t i = 0; i < n - 1; i++)
		{
			end = NextObj(end);
		}
		_freeList = NextObj(end);
		NextObj(end) = nullptr;//一定要注意置空
		_size -= n;
	}

	//这里的删除需要同时返回这个小对象
	void* Pop()
	{
		assert(_freeList);
		//头删
		void* obj = _freeList;
		_freeList = NextObj(obj);

		_size--;

		return obj;
	}

	size_t& MaxSize()
	{
		return _maxSize;
	}

	size_t Size()
	{
		return _size;
	}

	bool Empty()
	{
		return _freeList == nullptr;
	}

public:
	void* _freeList = nullptr;//自由链表
private:
	size_t _maxSize = 1;
	size_t _size = 0;
};


计算对象大小的对齐映射规则
class SizeClass
{
public:
	// 整体控制在最多10%左右的内碎片浪费
	// [1,128]                8byte对齐           freelist[0,16)
	// [128+1,1024]           16byte对齐          freelist[16,72)
	// [1024+1,8*1024]        128byte对齐         freelist[72,128)
	// [8*1024+1,64*1024]     1024byte对齐        freelist[128,184)
	// [64*1024+1,256*1024]   8*1024byte对齐      freelist[184,208)

	//向上对齐计算对象大小
	//由于这里是按照一定规则对齐的,所以其实是会有内存碎片的(内碎片),即申请1字节也是给8字节,申请2字节
	//也是给8字节的,但是根据以上计算可以控制最多10%左右的内碎片浪费。
	static inline size_t RoundUp(size_t bytes)
	{
		if (bytes <= 128)
		{
			return _RoundUp(bytes, 8);
		}
		else if (bytes <= 1024)
		{
			return _RoundUp(bytes, 16);
		}
		else if (bytes <= 8*1024)
		{
			return _RoundUp(bytes, 128);
		}
		else if (bytes <= 64*1024)
		{
			return _RoundUp(bytes, 1024);
		}
		else if (bytes <= 256*1024)
		{
			return _RoundUp(bytes, 8 * 1024);
		}
		else
		{
			//以页为单位对齐
			return _RoundUp(bytes, 1 << PAGE_SHIFT);
		}
	}

	//计算某一对象映射的哈希桶的下标
	static inline size_t Index(size_t alignSize)
	{
		assert(alignSize <= MAX_BYTES);

		//不同的对齐数对应的区间中有多少个哈希桶
		static int group[] = { 16,56,56,56 };

		if (alignSize <= 128)
		{
			return _Index(alignSize, 3);
		}
		else if (alignSize <= 1024)
		{
			return _Index(alignSize - 128, 4) + group[0];
		}
		else if (alignSize <= 8 * 1024)
		{
			return _Index(alignSize - 1024, 7) + group[0] + group[1];
		}
		else if (alignSize <= 64 * 1024)
		{
			return _Index(alignSize - 8 * 1024, 10) + group[0] + group[1] + group[2];
		}
		else if (alignSize <= 256 * 1024)
		{
			return _Index(alignSize - 64 * 1024, 13) + group[0] + group[1] + group[2] + group[3];
		}
		else
		{
			//alignSize太大就无法映射到哈希桶中了,这时应该直接断言错误即可
			assert(false);
			return -1;
		}
	}

	//计算当有线程申请一个size大小的内存块时,threadcache对应哈希桶没有内存块
	//时一次向central cache申请多少个size大小的内存块,多的挂到threadcache
	//对应的哈希桶中,这个是大佬们研究之后得出来的算法
	static size_t NumMoveSize(size_t size)
	{
		int n = MAX_BYTES / size;
		if (n < 2)
		{
			return 2;
		}
		else if (n > 512)
		{
			return 512;
		}
		else
		{
			return n;
		}
	}

	//计算当CentralCache对应位置的哈希桶中没有空闲的span的时候向PageCache
	//申请多少页大小的span对象,这个是别人通过测试得出来的算法
	static size_t NumMovePage(size_t size)
	{
		size_t num = NumMoveSize(size);
		size_t npage = num * size;
		npage >>= PAGE_SHIFT;
		if (npage == 0)
		{
			npage = 1;
		}
		return npage;
	}

private:
	static inline size_t _RoundUp(size_t bytes, size_t alignNum/*对齐数*/)
	{
		//对齐之后的大小
		//size_t alignSize;
		//if (bytes % alignNum == 0)
		//{
		//	alignSize = bytes;
		//}
		//else
		//{
		//	alignSize = (bytes / alignNum + 1) * alignNum;
		//}
		//return alignSize;

		return (bytes + alignNum - 1) & ~(alignNum - 1);
	}

	//static inline size_t _Index(size_t alignSize, size_t align)
	//{
	//	int index;
	//	if (alignSize % (1 << align) != 0)
	//	{
	//		index = alignSize / align;
	//	}
	//	else
	//	{
	//		index = alignSize / align - 1;
	//	}
	//	return index;
	//}

	static inline size_t _Index(size_t bytes, size_t align_shift)
	{
		return ((bytes + (1 << align_shift) - 1) >> align_shift) - 1;
	}
};

//以页为单位的大块内存,1页=8k=8*1024字节
struct Span
{
	PAGE_ID _pageId = 0;//Span大块内存的起始页号,页号相当于一个地址,描述这个大块内存的位置
	size_t _n = 0;//页数,代表这个Span有多少页内存,表示这个Span的大小

	Span* _next = nullptr;//用双向链表结构组织起来
	Span* _prev = nullptr;

	void* _freeList = nullptr;//用Span切好的小块内存的自由链表
	size_t _useCount = 0;//记录Span切好的小块内存被使用的数量,方便后续归还内存

	size_t _objSize = 0;//记录该span被切成的小块内存的大小,让释放内存时不用传大小

	//表示该span是否正在被使用,如果span在PageCache,则认为该span没有被使用
	//可以在PageCache和前后的空闲页合并,如果span被分到CentralCache,则说明
	//该span被使用,不能被合并
	bool _isUse = false;

};

//用带头双向链表结构把多个Span组织起来
class SpanList
{
public:
	SpanList()
	{
		//哨兵位的头节点
		_head = new Span;
		_head->_next = _head;
		_head->_prev = _head;
	}

	void PushFront(Span* newSpan)
	{
		Insert(Begin(), newSpan);
	}

	bool Empty()
	{
		return _head->_next == _head;
	}

	Span* PopFront()
	{
		Span* front = Begin();
		Erase(Begin());
		return front;
	}

	Span* Begin()
	{
		return _head->_next;
	}

	Span* End()
	{
		return _head;
	}


	void Insert(Span* pos, Span* newSpan)
	{
		assert(pos);
		assert(newSpan);

		Span* prev = pos->_prev;
		prev->_next = newSpan;
		newSpan->_prev = prev;
		newSpan->_next = pos;
		pos->_prev = newSpan;

	}
	void Erase(Span* pos)
	{
		assert(pos != _head);

		Span* prev = pos->_prev;
		Span* next = pos->_next;

		prev->_next = next;
		next->_prev = prev;
	}
private:
	Span* _head;

public:
	//桶锁,因为全局只有一个central cache,如果有多个线程同时访问同一个桶的时候
	//会有线程安全的问题,所以需要加锁,但是多个线程同时访问到同一个桶的概率不大
	//所以锁竞争没有那么激烈
	std::mutex _mtx;
};


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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

神经网络NLP基础 循环神经网络 LSTM

用的时候&#xff0c;只关心token的输入&#xff0c;以及hidden state就好了 sequence的length是多少&#xff0c;lstm的cell的数量就是多少 LSTM BI-LSTM stacked lstm GRU 实现

CANalyzer panel

(1205条消息) CAPL 脚本中对信号&#xff0c;系统变量&#xff0c;环境变量的 事件响应_capl programs脚本怎么写信号运算_蚂蚁小兵的博客-CSDN博客 注意环境变量是在工程关联的dbc中创建的&#xff1b;而系统变量是在CANoe工程工具栏的”Environment”下的”System Variables”…

使用axi_quad_spi操作spi_flash

文章目录 基本测试情况IP支持的命令 基本测试情况 有spi_flash需要访问&#xff0c;为简单计&#xff0c;选择使用axi_quad_spi进行操作。开始时&#xff0c;将IP配置成如下参数&#xff0c; 这样配置&#xff0c;是想着能够适应各家的FLASH&#xff08;实际使用的则是micron…

网易24届内推

【网易】2024届网易互联网秋季校园招聘内推开始啦&#xff01;给你分享我的专属内推邀请函&#xff1a;https://bole.campus.163.com/campus/home?projectId55&type99&isShare1&boleId7b842acc7c2b42db&boleType2&signatured5f2a3dc23bed70777a8be1a14b49…

【力扣每日一题】2023.8.31 一个图中连通三元组的最小度数

目录 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 代码&#xff1a; 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 题目给我们一个无向图&#xff0c;要我们找出三个节点&#xff0c;这三个节点他们两两相连&#xff0c;这三个节点除了连接到对方的其他线…

李宏毅 2022机器学习 HW2 strong baseline 上分路线

strong baseline上分路线 baseline增加concat_nframes &#xff08;提升明显&#xff09;增加batchnormalization 和 dropout增加hidden layer宽度至512 &#xff08;提升明显&#xff09; 提交文件命名规则为 prediction_{concat_nframes}[{n_hidden_layers}{dropout}_bn].c…

2023年8月随笔之有顾忌了

1. 回头看 日更坚持了243天。 读《发布&#xff01;设计与部署稳定的分布式系统》终于更新完成 选读《SQL经典实例》也更新完成 读《高性能MySQL&#xff08;第4版&#xff09;》开更&#xff0c;但目前暂缓 读《SQL学习指南&#xff08;第3版&#xff09;》开更并持续更新…

XSS漏洞及复现

一、什么是XSS 跨站脚本( Cross-site Scripting )攻击&#xff0c;攻击者通过网站输入框输入payload(脚本代码 )&#xff0c;当用户访问网页时&#xff0c;恶意payload自动加载并执行&#xff0c;以达到攻击者目的( 窃取cookie、恶意传播、钓鱼欺骗等)为了避免与HTML语言中的C…

EMQX启用双向SSL/TLS安全连接以及java连接

作为基于现代密码学公钥算法的安全协议&#xff0c;TLS/SSL 能在计算机通讯网络上保证传输安全&#xff0c;EMQX 内置对 TLS/SSL 的支持&#xff0c;包括支持单/双向认证、X.509 证书、负载均衡 SSL 等多种安全认证。你可以为 EMQX 支持的所有协议启用 SSL/TLS&#xff0c;也可…

java+jsp+servlet+mysql蛋糕商城

项目介绍&#xff1a; 本系统为基于jspservletmysql的蛋糕商城&#xff0c;包含管理员和用户角色&#xff0c;用户功能如下&#xff1a; 用户&#xff1a;注册、登录系统&#xff1b;查看商品分类&#xff1b;查看热销、新品商品&#xff1b;查看商品详情&#xff1b;搜索商品…

数据结构体--5.0图

目录 一、定义 二、图的顶点与边之间的关系 三、图的顶点与边之间的关系 四、连通图 五、连通图的生成树定义 一、定义 图&#xff08;Graph&#xff09;是由顶点的又穷非空集合合顶点之间边的集合组成&#xff0c;通常表示为&#xff1a;G&#xff08;V&#xff0c;E&…

【C++】 C++11(右值引用,移动语义,bind,包装器,lambda,线程库)

文章目录 1. C11简介2. 统一的列表初始化2.1 &#xff5b;&#xff5d;初始化2.2 std::initializer_list 3. 声明3.1 auto3.2 decltype3.3 auto与decltype区别3.4 nullptr 4. 右值引用和移动语义4.1 左值引用和右值引用4.2 左值引用与右值引用比较4.3 右值引用使用场景和意义4.…

《论文阅读18》JoKDNet

一、论文 研究领域&#xff1a;用于大尺度室外TLS点云配准的联合关键点检测和特征表达网络论文&#xff1a;JoKDNet: A joint keypoint detection and description network for large-scale outdoor TLS point clouds registration International Journal of Applied Earth Ob…

UI自动化之关键字驱动

关键字驱动框架&#xff1a;将每一条测试用例分成四个不同的部分 测试步骤&#xff08;Test Step&#xff09;&#xff1a;一个测试步骤的描述或者是测试对象的一个操作说明测试步骤中的对象&#xff08;Test Object&#xff09;&#xff1a;指页面的对象或者元素对象执行的动…

访问0xdddddddd内存地址引发软件崩溃的问题排查

目录 1、问题描述 2、访问空指针或者野指针 3、常见的异常值 4、0xdddddddd内存访问违例问题分析与排查 5、关于0xcdcdcdcd和0xfeeefeee异常值的排查案例 6、最后 VC常用功能开发汇总&#xff08;专栏文章列表&#xff0c;欢迎订阅&#xff0c;持续更新...&#xff09;ht…

【ag-grid-vue】列定义(Updating Column Definitions)

列定义一节解释了如何配置列。可以在初始设置列之后更改列的配置。本节介绍如何更新列定义。 添加和删除列 可以通过更新提供给网格的列定义列表来添加和删除列。当设置新列时&#xff0c;网格将与当前列进行比较&#xff0c;并计算出哪些列是旧的(要删除)、哪些列是新的(创建…

什么是 TF-IDF 算法?

简单来说&#xff0c;向量空间模型就是希望把查询关键字和文档都表达成向量&#xff0c;然后利用向量之间的运算来进一步表达向量间的关系。比如&#xff0c;一个比较常用的运算就是计算查询关键字所对应的向量和文档所对应的向量之间的 “相关度”。 简单解释TF-IDF TF &…

第一个react应用程序并添加样式

编写第一个react应用程序 将目录下的文件、src文件夹、public文件夹清空&#xff0c;项目根目录下新建一个文件index.js 在文件中写入以下代码 import React from react import ReactDOM from react-dom ReactDOM.render(<h1>欢迎进入React的世界</h1>,document.…

编译工具:CMake(六) | 使用外部共享库和头文件

编译工具&#xff1a;CMake&#xff08;六&#xff09; | 使用外部共享库和头文件 步骤引入头文件搜索路径为 target 添加共享库 步骤 在/Compilation_tool/cmake 目录建立 t4 目录 建立src目录&#xff0c;编写源文件main.c&#xff0c;内容如下&#xff1a; #include <…

MybatisPlus(2)

前言&#x1f36d; ❤️❤️❤️SSM专栏更新中&#xff0c;各位大佬觉得写得不错&#xff0c;支持一下&#xff0c;感谢了&#xff01;❤️❤️❤️ Spring Spring MVC MyBatis_冷兮雪的博客-CSDN博客 上篇我们简单介绍了MybatisPlus的方便之处&#xff0c;这篇来深入了解Myb…