简化版高并发内存池

本文详细介绍了简化版高并发内存池的设计思路,包括内存池的概念、定长内存池的创建和释放,以及高并发内存池的整体结构框架。重点探讨了线程缓存(thread cache)、中心缓存(central cache)和页缓存(page cache)的实现,特别是锁竞争的优化。此外,还讨论了大于256KB的大块内存申请问题和释放对象时的优化。最后,通过性能瓶颈分析引入基数树进行无锁优化。
摘要由CSDN通过智能技术生成

一、项目介绍

简化版高并发内存池是基于Google 的一个开源项目TCMalloc,TCMalloc 是 Google 开发的内存分配器,全称为Thread-Caching-Malloc,即线程缓存的malloc,实现高效的多线程内存管理。
在这里插入图片描述

1.1、项目知识要求

这个项目涉及C/C++、数据结构(链表、哈希桶)、操作系统的内存管理、单例模式、多线程、互斥锁等。

二、内存池

2.1、池化技术

所谓的**“池化技术”**就是向系统申请一个过量的资源,然后自己管理,类似的现象就是比如向家里人要生活费,索要生活费可大致分为两种,一种就是每天需要多少生活费,就问家里人要多少生活费,另外一种就是,一次要一周的生活费自己管理。池化技术就类似后者,可以避免每次都问系统申请资源,提高程序运行效率。

2.2、内存池

内存池百度百科
内存池就是指程序先从操作系统中申请一块足够大的内存,此后,当程序需要申请内存时,不是直接向操作系统申请,而是直接从内存池中获取;同理,当程序释放内存时,并不是直接将内存返回给操作系统,而是返回内存池。当程序退出时,内存池将之前申请的内存释放。

2.3、内存池主要解决什么问题

1、效率问题
针对效率问题,如前面说的生活费问题,可以避免多次向操作系统申内存。
2、内存碎片的问题
a、内存碎片是怎么产生的?
在这里插入图片描述
如上图所示,vector向系统申请了256Byte的空间,然后又释放给系统,list向系统申请128Byte的空间,然后也释放了,我们现在又三百多Byte的空间,现在想申请三百多Byte的空间,但是会申请失败,因为系统中的那三百多Byte的空间是不连续的,所以哪些空间就成为了内存碎片。

三、定长内存池

定长内存池就是空间大小固定的内存。
定长内存池之所以高效,是因为它可以切除固定大小的内存供线程使用。

3.1如何设计一个定长内存池?

3.1.1定长内存池中有什么?

定长内存池包括:一个大块内存(内存池),一个用于链接释放空间的自由链表,用于计算大块内存在切分后剩余空间大小。

private:
	//指向大块内存的指针
	char* _memory=nullptr;//为什么要用char?因为一个char就是一个在字节
	void* _freeList = nullptr;//还回过程中链接的自由链表的指针
	size_t _remainBytes = 0;//大块内存剩余的字节数

3.1.2创建内存池

首先直接向系统申请一大块内存,用来做内存池
在这里插入图片描述
申请完后就可以使用该内存池,当我们释放内存时,该内存就会被挂在自由链表中(回收内存链表),所以,我们应该首先判断自由链表中有没有已经回收了的内存,如果有,我们优先使用该链表中挂的内存,否则直接在内存池中申请内存。

T* New()
	{
   
		T* obj;
		//优先使用_freelist中的空间
		if (_freeList)
		{
   
			void* next = *(void**)_freeList;//存储地址,下一个内存块的地址——》指向下一个内存块
			obj =(T*) _freeList;
			_freeList = next;
			
		}
		else
		{
   
			//剩余内存不够一个人对象大小时,重新开一个大空间
			if (_remainBytes <sizeof(T))
			{
   
				_remainBytes = 128 * 1024;
				//_memory = (char*)malloc(_remainBytes);
				//直接调用系统
				_memory = (char*)SystemAlloc(_remainBytes >> 13);
				if (_memory == nullptr)
				{
   
					throw std::bad_alloc();
				}
				/*T* obj = (T*)_memory;
				size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
				_memory += objSize;
				_remainBytes -= sizeof(T);*/
			}
			obj = (T*)_memory;
			size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
			_memory += objSize;
			_remainBytes -= objSize;
			
		}
		//因为这里只是开了空间,并没有初始化,所以需要调用函数的我初始化
		//定位new,显示调用T的构造函数初始化
		new(obj)T;
		return obj;
	}

3.1.3释放内存

当内存不用之后,直接用链表将释放的内存块挂起来

在这里插入图片描述我们这里将第一块内存中的一部分用来存放下一内存的地址,这样就可以像链表一样将内存块挂起来

//释放内存
	void Delete(T* obj)
	{
   
		//显示调用析构函数
		obj->~T();
		//if (_freeList == nullptr)
		//{
   
		//	_freeList = obj;
		//	//*(int*) 对int*解引用就会指向四个字节
		//	//*(void**)在32或者64位下分别是4字节和8字节,刚好可以存一个地址
		//	*(void**)obj = nullptr;//首先先让这个返回的自由链表的前四个字节指向nullptr
		//}
		//else
		//{
   
		//	*(void**)obj == _freeList;
		//	_freeList = obj;
		//}
		//直接头插就可以,不需要去判断是否为空
		*(void**)obj = _freeList;
		_freeList = obj;

	}

3.2整体代码

#pragma once
#include"Common.h"
//定长内存池 
template<class T>
class ObjectPool
{
   
public:
	//申请内存
	T* New()
	{
   
		T* obj;
		//优先使用_freelist中的空间
		if (_freeList)
		{
   
			void* next = *(void**)_freeList;//存储地址,下一个内存块的地址——》指向下一个内存块
			obj =(T*) _freeList;
			_freeList = next;
			
		}
		else
		{
   
			//剩余内存不够一个人对象大小时,重新开一个大空间
			if (_remainBytes <sizeof(T))
			{
   
				_remainBytes = 128 * 1024;
				//_memory = (char*)malloc(_remainBytes);
				//直接调用系统
				_memory = (char*)SystemAlloc(_remainBytes >> 13);
				if (_memory == nullptr)
				{
   
					throw std::bad_alloc();
				}
				/*T* obj = (T*)_memory;
				size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
				_memory += objSize;
				_remainBytes -= sizeof(T);*/
			}
			obj = (T*)_memory;
			size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
			_memory += objSize;
			_remainBytes -= objSize;
			
		}
		//因为这里只是开了空间,并没有初始化,所以需要调用函数的我初始化
		//定位new,显示调用T的构造函数初始化
		new(obj)T;
		return obj;
	}
	//释放内存
	void Delete(T* obj)
	{
   
		//显示调用析构函数
		obj->~T();
		//if (_freeList == nullptr)
		//{
   
		//	_freeList = obj;
		//	//*(int*) 对int*解引用就会指向四个字节
		//	//*(void**)在32或者64位下分别是4字节和8字节,刚好可以存一个地址
		//	*(void**)obj = nullptr;//首先先让这个返回的自由链表的前四个字节指向nullptr
		//}
		//else
		//{
   
		//	*(void**)obj == _freeList;
		//	_freeList = obj;
		//}
		//直接头插就可以,不需要去判断是否为空
		*(void**)obj = _freeList;
		_freeList = obj;

	}
private:
	//指向大块内存的指针
	char* _memory=nullptr;//为什么要用char?因为一个char就是一个在字节
	void* _freeList = nullptr;//还回过程中链接的自由链表的指针
	size_t _remainBytes = 0;//大块内存剩余的字节数
}; 
struct TreeNode
{
   
	int _val;
	TreeNode* _left;
	TreeNode* _right;
	TreeNode()
		:_val(0)
		, _left(nullptr)
		, _right(nullptr)
	{
   }
};
//测试用例
void TestObjectPool()
{
   
	//申请释放的轮次
	const size_t Rounds = 5;
	//每轮申请释放多少次
	const size_t N = 100000;
	std::vector<TreeNode*> v1;
	v1.reserve(N);
	size_t begin1 = clock();
	for (size_t j0 = 0; j0 < Rounds; ++j)
	{
   
		for (int i = 0; i < N; ++i)
		{
   
			v1.push_back(new TreeNode);
		}
		for (int i = 0; i < N; ++i)
		{
   
			delete v1[i];
		}
	}
	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 toime:" << end1 - begin1 << endl;
	cout << "object pool cost time:" << end2 - begin2 << endl;


}    

3.2.1测试结果

在这里插入图片描述

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

在这里插入图片描述
在实现内存池时,我们一般需要考虑到效率问题和内存碎片问题,但是对于高并发内存池来说,我们不仅需要考虑以上问题,还需要考虑在多线程环境下的锁竞争问题
高并发内存池整体框架由以下几个部分组成:thread cache(线程缓存)、central cache(中心缓存)、page cache(页缓存)。

4.0各个部分的主要作用

  1. thread cache主要解决的是锁竞争的问题,每一个线程独享自己的thread cache,当thread cache中有内存时,该线程就不会和其他线程进行竞争,每个线程只需要在自己的thread cache中申请内存就好。
  2. central cache主要起到一个居中调度的作用,每一个线程的thread cache需要从central cache中获取内存,如果thread cache中的内存有很多的时候,就会将内存还给central cache,其作用类似于中枢,起到调节的作用,所以被称为中心缓存。
  3. page cache就负责提供以页为单位的大块内存,当central cache需要内存时,就会向page cache申请,而当page cache中没有足够多的内存时,就会直接向系统进行申请。

4.1thread cache(线程缓存)

a、线程缓存就是每个线程独有一个线程缓存空间,用于小于256KB的内存的分配,线程从这里申请内存不需要加锁,这样不经可以保证线程与线程之间的独立,并且可以保证并发高效。
b、定长内存池只支持固定大小的内存块的申请,因此定长内存池只需要一个自由链表用来管理释放回来的内存块(固定大小的内存块)。如果我们想要申请和释放不同大小的内存块,那么就需要多个自由链表来管理释放回来的内存块,因此thread cache实际上是一个哈希桶的结构,每个桶上挂着不同大小的自由链表。
c、我们需要设计所有大小的内存块吗?不是的,如果我们要设计所有大小的内存块,我们就需要20多万个自由链表,这样无疑是一个很大的工程,并且只是用来存储这些自由链表的头指针就要消耗大量的内存。所有我们采用按照某种规则对这些字节数进行对齐,这里我们采用的是8字节对齐规则(因为在64位下,8字节刚好可以存储头指针),比如我们申请1~8字节大小的内存时,thread cache 直接就给我们8字节,如果申请9~16字节时,就直接给我们16字节大小的内存块。

在这里插入图片描述
但是当在多线程情况下,thread cache 可能会同时去central cache 申请内存,此时就会涉及线程安全的问题,因此在访问central cache时需要加锁,但是central cache实际上是一个哈希桶的结构,只有当多个线程同时访问一个桶的时候才会加锁,所以这里的锁竞争问题不是很激烈。

4.1.1thread cache 包含的内容
a、插入:push---------》头插
在这里插入图片描述

void push(void* obj)
	{
   
		//头插
		//*(void**)obj = _freeList;//先将obj强转成void**,这样就会取到前四或者八个个字节(地址)
		NextObj(obj) = _freeList;
		_freeList = obj;
		++_size;
	}

b、删除(弹出):pop--------》头删
在这里插入图片描述

void* pop()
	{
   
		//头删
		assert(_freeList);
		void* obj = _freeList;
		_freeList = NextObj(obj);
		--_size;
		return obj;
	}
threadcache哈希桶映射对齐规则

在这里插入图片描述
虽然对齐产生的内碎片会引起一定程度的空间浪费,但按照上面的对齐规则,我们可以将浪费率控制到百分之十左右。需要说明的是,1~128这个区间我们不做讨论,因为1字节就算是对齐到2字节也有百分之五十的浪费率。
在这里插入图片描述

对齐和映射相关函数的编写:
我们需要提供两个对应的函数,分别用于获取某个大小字节数对齐后的字节数,以及该字节数对用的哈希桶的下标(当释放该内存时,方便将该内存块归还)

//管理对齐和映射等关系
class SizeClass
{
   
public:
	//获取向上对齐后的字节数
	static inline size_t RoundUp(size_t bytes);
	//获取对应哈希桶的下标
	static inline size_t Index(size_t bytes);
};

获取对齐后的字节数:我们需要先判断该申请的内存块位于哪一个区间,然后再通过调用子函数进行进一步的处理。

//获取向上对齐后的字节数
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);
	
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值