C++_项目高并发内存池——优化(定长内存池配合脱离new申请空间)(释放空间时不需要传大小)

1. 定长内存池配合脱离new申请空间

根据C++项目高并发内存池_定长内存池可知,使用定长内存池比直接new申请对象效率要高。

在PageCache向系统申请内存时我们使用new创建了许多Span对象来管理向系统申请的大块内存,这个Span对象是new出来的。以及在线程创建属于自己的ThreadCache时,也使用了new来申请ThreadCache对象,我们可以联系定长内存池,把这些申请过程用定长内存池替代,效率更高。

定长线程池:

#pragma once

#include"Common.h"

template<class T>//定长内存池
class ObjectPool {
private:
	char* _memory;//指向大块内存的指针
	//返回的内存用链式结构管理
	void* _freeList;
	size_t _overage;//大块内存剩余空间大小
public:
	ObjectPool() :_memory(nullptr), _freeList(nullptr),_overage(0) {}

	T* New() {
		T* obj = nullptr;
		if (_freeList != nullptr) {
			//优先把归还的内存重复利用
			//链表头删
			void* next = *((void**)_freeList);
			obj = (T*)_freeList;
			_freeList = next;
		}
		else
		{
			if (_overage < sizeof(T)) {
				_overage = 100 * 1024;
				_memory = (char*)SystemAlloc(_overage);
				if (_memory == nullptr) {
					throw std::bad_alloc();
				}
			}
			obj = (T*)_memory;
			size_t SizeT = sizeof(T) > sizeof(void*) ? sizeof(T) : sizeof(void*);
			_memory += SizeT;
			_overage -= SizeT;
		}
		//调用定位new进程空间初始化
		new(obj)T;
		return obj;
	}

	void Delete(T* obj) {
		obj->~T();//显示调用析构函数,清理对象
		//头插
		*(void**)obj = _freeList;
		_freeList = obj;
	}
};

所以在PageCache中需要新定义成员定长内存池

#pragma once

#include"Common.h"
#include"ObjectPool.h"

class PageCache {
private:
	SpanList _SpanList[NPAGE];
	static PageCache _sInst;

	//页号与Span链表的映射,方便归还内存时直接通过内存找页号找到这块内存是那个Span
	std::unordered_map<PAGE_ID, Span*>IdSpanMap;

	//定长内存池来申请Span对象
	ObjectPool<Span>_SpanPool;

	PageCache() {}
public:
	std::mutex _PageMtx;

	PageCache(const PageCache&) = delete;

	//获取这个内存是那个Span
	Span* MapObjToSpan(void* obj);

	static PageCache* GetInst() {
		return &_sInst;
	}

	//将CentralCache的Span回收,合并相邻页
	void RetrunPageCache(Span* span);

	//获取NumPage页的Span
	Span* NewSpan(size_t NumPage);
};

在PageCache中每当申请/释放Span时用定长内存池New/Delete

线程在获得自己的Thread Cache时也使用定长内存池来申请对象

#pragma once

#include"Common.h"
#include"ThreadCache.h"
#include"PageCache.h"
#include"ObjectPool.h"

//线程调用申请ThreadCache空间
static void* ConcurrentAlloc(size_t size) {
	if (size > MAX_BYTE) {//大于256KB内存
		size_t AlignSize = SizeClass::RoundUp(size);//计算对其大小
		//直接向PageCache索要K页的内存
		size_t K = AlignSize >> PAGESIZE;
		PageCache::GetInst()->_PageMtx.lock();
		Span*span=PageCache::GetInst()->NewSpan(K);
		PageCache::GetInst()->_PageMtx.unlock();
		void* ptr = (void*)(span->_PageID << PAGESIZE);//获取这块内存的地址
		return ptr;
	}
	else {
		//获取线程自己的ThreadCache
		if (tls_threadcache == nullptr) {
			static ObjectPool<ThreadCache>TcPool;//使用定长内存池来获取对象
			tls_threadcache = TcPool.New();
		}
		cout << std::this_thread::get_id() << " " << tls_threadcache << endl;
		return tls_threadcache->ApplySpace(size);
	}
}

static void ConcurrentFree(void* ptr, size_t size) {
	if(size>MAX_BYTE){
		Span* span = PageCache::GetInst()->MapObjToSpan(ptr);//计算要释放的大空间属于那个Span

		PageCache::GetInst()->_PageMtx.lock();
		PageCache::GetInst()->RetrunPageCache(span);//将内存挂到PageCache桶上,需要修改桶,所以要加锁
		PageCache::GetInst()->_PageMtx.unlock();
	}
	else {
		//释放时每个线程一定有tls_threadcache
		assert(tls_threadcache != nullptr);
		tls_threadcache->ReleaseSpace(ptr, size);
	}
}

因为修改代码变化不大,为了避免冗余这里贴出链接

Github
Gitee

2. 释放空间时不需要传大小

在之前,我们的高并发内存池释放空间时需要传大小,十分的不方便。

传大小来释放空间,为了判断这块空间是从堆上申请的还是通过三层缓存来申请的。还可以判断这块内存在ThreadCache和CentralCache哈希桶中的那个桶。

因为PageCache中,已经实现了页号到Span*的映射。
所以知道要释放的地址,根据地址计算页号,在根据页号找到Span,每个Span中自由链表上挂的空间大小都是相同的,所以在Span中添加属性,记录切好的内存块的大小。

//管理以页为单位的大块空间结构
struct Span {
	PAGE_ID _PageID;//记录是第几页
	size_t _Num;//记录Span里面有多少页
	Span* _next;//双向链表
	Span* _prev;

	size_t use_count;//记录分配了多少个对象给ThreadCahce

	void* FreeList;//切好的小块内存空间

	bool IsUse;//标记这块Span是否被使用

	size_t ObjectSize;//每个切好的小块内存空间大小

	Span() :_PageID(0), _Num(0), _next(nullptr), _prev(nullptr), use_count(0), FreeList(nullptr), IsUse(false), ObjectSize(0) {}
};
  • 在Central Cache中将PageCache的大块内存切分成小内存时,就将Span中 ObjectSize字段记录下来。等到最后释放时根据页号找Span在找 ObjectSize字段就知道要释放内存的大小了。
  • 如果申请大于256KB,没有经过CentralCache,直接向PageCache申请,此时需要在申请后手动填写 ObjectSize字段。

注意:
大于256KB时访问PageCache的哈希桶,而PageCache只能被一个线程访问,PageCache中的Span指针与页号的映射也是能由一个人访问,所以要对获取Span映射的函数添加锁。

Span* PageCache::MapObjToSpan(void* obj) {

	std::unique_lock<std::mutex>lock(_PageMtx);//映射被多个线程访问需要加锁防止线程安全,出了函数锁自动释放
	//计算obj的页号
	PAGE_ID pageId = (PAGE_ID)obj >> PAGESIZE;
	//获取这个内存是那个Span
	std::unordered_map<PAGE_ID,Span*>::iterator ret = IdSpanMap.find(pageId);
	if (ret != IdSpanMap.end()) {
		return ret->second;
	}
	else {
		assert(false);//不可能找不到
		return nullptr;
	}
}

因为修改代码变化不大,为了避免冗余这里贴出链接

Github
Gitee

  • 5
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
老师可能会提出以下问题: 1. 什么是内存?为什么需要内存? 答:内存是一种内存管理技术,用于提高内存分配和释放的性能。它通过预先分配一定数量的内存块,并在程序运行期间重复利用这些内存块来避免频繁的内存分配和释放操作,从而提高程序的运行效率。 2. 内存如何实现高并发? 答:内存可以通过多线程技术来实现高并发。一般情况下,内存会将内存块分配给不同的线程进行使用,每个线程都有自己的内存。当多个线程同请求内存内存可以进行加锁操作来保证线程安全。 3. 如何处理内存中的内存碎片问题? 答:内存中的内存碎片问题可以通过两种方式来解决。一种是使用内存的分配算法来减少内存碎片的产生,另一种是定期对内存进行整理和重组,以消除已有的内存碎片。 4. 如何进行内存的扩展和收缩? 答:内存的扩展和收缩可以通过动态调整内存大小来实现。当内存中的内存块被耗尽,可以重新分配一定数量的内存块,并将它们添加到内存中。当内存中的内存块处于空闲状态,可以将它们从内存中移除,以释放内存空间。 5. 如何测试内存的性能? 答:测试内存的性能可以使用一些基准测试工具,如Google Benchmark等。在测试,可以比较内存的分配和释放操作与系统默认的分配和释放操作之间的性能差异。同,还可以测试内存在高并发环境下的性能表现。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

NUC_Dodamce

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值