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

news2025/1/16 2:10:08

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

现代很多的开发环境都是多核多线程,在申请内存的场景下,必然存在激烈的锁竞争问题。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;
};


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

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

相关文章

使用Mars3d的XyzLayer,通过zIndex使得图层叠加在最上面

问题&#xff1a;XyzLayer的温度图设置了zIndex: 999,之后&#xff0c;依然会被后加入的电子地图覆盖 // 叠加的图层 let tileLayer let tileLayer1 export function addTileLayer() { removeTileLayer() // 方式2&#xff1a;在创建地球后调用addLayer添加图层(直接new对应…

C++11——右值引用和移动语义

✅<1>主页&#xff1a;&#xff1a;我的代码爱吃辣 &#x1f4c3;<2>知识讲解&#xff1a;C11——右值引用 ☂️<3>开发环境&#xff1a;Visual Studio 2022 &#x1f4ac;<4>前言&#xff1a;右值引用&#xff0c;是C11更新的一个非常有价值的语法&am…

kali虚拟机

kali网络配置 虚拟网络编辑器配置 打开虚拟网络编辑器&#xff0c;用管理员权限打开 打开后 VMnet0采用桥接模式&#xff0c;外部连接这边选择自己桥接的对象 控制面板可查看桥接对象 VMnet8这边选择NAT模式 VMnet1选择主机模式 因为要用到两个网卡&#xff0c; 所以我们在…

流程制造智能工厂总体架构及建设路线规划方案PPT

本资料来源公开网络&#xff0c;仅供个人学习&#xff0c;请勿商用&#xff0c;如有侵权请联系删除&#xff0c;更多浏览公众号&#xff1a;智慧方案文库 数字孪生智能制造(智改数转)数字化架构设计及应用..水泥智能工厂解决方案.pptx智慧制造规划设计解决方案.pptx智能工厂落…

Day52|动态规划part13:300.最长递增子序列、674. 最长连续递增序列

300. 最长递增子序列 leetcode链接&#xff1a;力扣题目链接 视频链接&#xff1a;动态规划之子序列问题&#xff0c;元素不连续&#xff01;| LeetCode&#xff1a;300.最长递增子序列 给你一个整数数组 nums &#xff0c;找到其中最长严格递增子序列的长度。子序列 是由数…

Docker资源控制cgroups

文章目录 一、docker资源控制1、资源控制工具2、Cgroups四大功能 二、CPU 资源控制1、设置CPU使用率上限2、CPU压力测试3、Cgroups限制cpu使用率4、设置CPU资源占用比&#xff08;设置多个容器时才有效&#xff09;5、设置容器绑定指定的CPU 三、对内存使用的限制四、对磁盘IO配…

【C++】—— c++11之智能指针

前言&#xff1a; 本期&#xff0c;我们将要学习的是在c11中新提出的概念——异常指针&#xff01; 目录 &#xff08;一&#xff09;智能指针的引入 &#xff08;二&#xff09;内存泄漏 1、什么是内存泄漏&#xff0c;内存泄漏的危害 2、内存泄漏分类&#xff08;了解&a…

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

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

循环购模式:美妆行业的新趋势

美妆是一种能够提升自信和魅力的艺术&#xff0c;它让每个人都可以展现自己的个性和风格。但是&#xff0c;美妆也是一种需要不断更新和学习的技能&#xff0c;它需要消费者投入时间和金钱&#xff0c;才能找到适合自己的产品和方法。有没有一种方式&#xff0c;可以让美妆变得…

用python画一个柱状图可能用到的代码【完整版】

画柱状图 导入包 import torch as t import numpy as np import pandas as pd import matplotlib.pyplot as plt import joblib import matplotlib as mpl设置默认字体格式为"Times New Roman" font_name Times New Roman mpl.rcParams[font.family] font_name通…

stable diffusion实践操作-提示词插件安装与使用

本文专门开一节写提示词相关的内容&#xff0c;在看之前&#xff0c;可以同步关注&#xff1a; stable diffusion实践操作 正文 1、提示词插件安装 1.1、 安装 1.2 加载【应用更改并重载前端】 1.3 界面展示 1.3.-4 使用 里面有个收藏列表&#xff0c;可以收藏以前的所有提示…

STM32f103入门(7)pwm驱动led驱动舵机驱动直流电机

PWM驱动 PWM介绍TIM_OC1Init 配置通道TIM_OCStructInit 输出比较参数默认值输出比较模式 TIM_OCInitstructure输出比较极性 TIM_OCInitstructure设置输出使能以下三个决定了PWM的频率 占空比初始化通道 TIM_OC1Init(TIM2, &TIM_OCInitstructure);GPIO复用 PWM通道 驱动LED复…

2022年09月 C/C++(五级)真题解析#中国电子学会#全国青少年软件编程等级考试

第1题&#xff1a;城堡问题 1 2 3 4 5 6 7 ############################# 1 # | # | # | | # #####—#####—#—#####—# 2 # # | # # # # # #—#####—#####—#####—# 3 # | | # # # # # #—#########—#####—#—# 4 # # | | | | # # ############################# (图 1)…

关于git约定式提交IDEA

背景 因为git提交的消息不规范导致被乱喷&#xff0c;所以领导统一规定了约定式提交 官话 约定式提交官网地址 约定式提交规范是一种基于提交信息的轻量级约定。 它提供了一组简单规则来创建清晰的提交历史&#xff1b; 这更有利于编写自动化工具。 通过在提交信息中描述功能…

【算法】递归的概念、基本思想

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞 关注支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; &#x1f525;c系列专栏&#xff1a;C/C零基础到精通 &#x1f525; 给大…

LuatOS 开发指南

NDK 开发 官方教程 官方例程 API 下载软件 下载官方NDK例程压缩包到本地&#xff0c;并解压。可以看到目录如下&#xff1a; doc: 文档教程 env: 编译环境 example: NDK示例 platform: 需要编译的平台&#xff08;air72x/air8xx&#xff09; tools: 其他辅助软件 VSCode 使…

问道管理:成交量买卖公式?

跟着股票商场的如火如荼&#xff0c;人们对于怎么解读和使用成交量进行股票生意的需求日积月累。成交量是指在某一特定时间内进行的股票生意的数量&#xff0c;它是投资者们研判商场状况和制定生意战略的重要指标之一。那么&#xff0c;是否存在一种最厉害的成交量生意公式呢&a…

day31 集合

一、Collection 创建对象 Collection c3 new HashSet(); //元素不可重复 无序 Collection c1 new ArrayList(); //元素可重复 有序collection方法 c.add() 添加引用类型数据 c.addAll() 添加collection对象 c.isEmpty() 判断是否为空 c.clear() 清空所有类容 c.…

深入了解Docker镜像操作

Docker是一种流行的容器化平台&#xff0c;它允许开发者将应用程序及其依赖项打包成容器&#xff0c;以便在不同环境中轻松部署和运行。在Docker中&#xff0c;镜像是构建容器的基础&#xff0c;有些家人们可能在服务器上对docker镜像的操作命令不是很熟悉&#xff0c;本文将深…

xx音乐app逆向分析

目标 看一下评论的请求 抓包 这里使用httpcanary 请求包如下 POST /index.php?rcommentsv2/getCommentWithLike&codeca53b96fe5a1d9c22d71c8f522ef7c4f&childrenidcollection_3_1069003079_330_0&kugouid1959585341&ver10&clienttoken7123ecc548ec46d…