高并发内存池(六):补充内容

目录

有关大于256KB内存的申请和释放处理方法 

处理大于256KB的内存申请

补充内容1

补充内容2

补充内容3 

处理大于256KB的内存释放

新增内容1

新增内容2

测试函数

使用定长内存池替代new

释放对象时不传对象大小

补充内容1

补充内容2

 补充内容3

补充内容4

测试函数

为哈希桶加锁

多线程环境下对比malloc测试

复杂问题的调试技巧

性能瓶颈分析

针对性能瓶颈使用基数树进行优化

基数树代码


有关大于256KB内存的申请和释放处理方法 

内存申请和释放大于256KB也分为两种情况:

  1. 大于256KB但是小于128 * 8 * 1024KB
  2. 大于128 * 8 * 1024KB

内存申请大于256KB但小于128 * 8 * 1024KB:因为PageCache中存放的span所管理的最大页数为128,即span可分配的最大内存为128 * 8 * 1024 KB,所以当可以直接向PageCache申请一个合适的span(该span管理的页数为此次内存申请对齐后大小 / 页大小)

内存申请大于128 * 8 *1024 KB:PageCache中已经没有合适的span了,直接向堆上申请

处理大于256KB的内存申请

补充内容1

在RoundUp函数中新增用于处理大于256KB内存申请的内存对齐判断

(_RoundUp函数不变)

//用于内存对齐
static inline size_t RoundUp(size_t size)
{
	if (size <= 128)
	{
		return _RoundUp(size, 8);
	}
	else if (size <= 1024)
	{
		return _RoundUp(size, 16);
	}
	else if (size <= 8 * 1024)
	{
		return _RoundUp(size, 128);
	}
	else if (size <= 64 * 1024)
	{
		return _RoundUp(size, 1024);
	}
	else if (size <= 256 * 1024)
	{
		return _RoundUp(size, 8 * 1024);
	}
	else
	{
		return _RoundUp(size, 1 << PAGE_SHIFT);//对齐数为当前规定的页的大小
	}
}

补充内容2

在NewSpan开始处中新增获取管理页数大于128的span的判断

//所需页数大于128PageCache无法分配,需要向堆申请
if (k > NPAGES - 1)
{
	void* ptr = SystemAlloc(k);//从堆获取一块内存

	Span* span = new Span;//申请一个span结点

	span->_PageId = (size_t)ptr >> PAGE_SHIFT;//新span中的页号为ptr指向的内存地址 / 页大小
	span->_n = k;//新span中的页数为n个

	_idSpanMap[span->_PageId] = span;//存放映射关系,即使该span不会被挂在PageCache上
	return span;
}

补充内容3 

在ConcurrentAlloc函数新增size > MAX_BYTES的判断

//申请内存
static void* ConcurrentAlloc(size_t size)
{
    if (size > MAX_BYTES)
    {
	    size_t alignSize = SizeClass::RoundUp(size);//内存对齐
	    size_t kpage = alignSize >> PAGE_SHIFT;//计算所需页数

	    PageCache::GetInstance()->_pageMtx.lock();
	    Span* span = PageCache::GetInstance()->NewSpan(kpage);//向PageCache申请管理kpage个页的span,若kpage > 128则需要经过补充内容2中的内容,否则还是按照原NewSpan执行
	    void* ptr = (void*)(span->_PageId << PAGE_SHIFT);//通过页号计算地址
	    PageCache::GetInstance()->_pageMtx.unlock();

	    return ptr;//返回从PageCache分配给的内存空间的地址
    }
	else
	{
	    ...//获取TLS的那部分内容
	}
}

处理大于256KB的内存释放

新增内容1

在ReleaseSpanToPageCache开始处新增当归还的span的_n > 128的判断

//大于128页的span直接还给堆
if (span->_n > NPAGES - 1)
{
	void* ptr = (void*)(span->_PageId << PAGE_SHIFT);
	SystemFree(ptr);
	delete span;
	return; 
}

新增内容2

在ConcurrentFree中新增用于size > MAX_BYTES的判断

//释放内存
static void ConcurrentFree(void* ptr,size_t size)
{
	if (size > MAX_BYTES)
	{
		Span* span = PageCache::GetInstance()->MapObjectToSpan(ptr);//根据归还的内存地址获取要归还的span

		PageCache::GetInstance()->_pageMtx.lock();
		PageCache::GetInstance()->ReleaseSpanToPageCache(span);//将要归还的span挂在PageCache上,或者返还给堆
		PageCache::GetInstance()->_pageMtx.unlock();
	}
	else
	{
        ...//还是原来的那两行释放内容
	}
}

测试函数

//用于测试内存申请和释放大于256KB
void BigAlloc()
{
    //内存申请和释放大于256KB,但是小于128 * 8 * 1024KB
	void* p1 = ConcurrentAlloc(257 * 1024);
	ConcurrentFree(p1, 257 * 1024);

    //内存申请和释放大于128 * 8 * 1024KB
	void* p2 = ConcurrentAlloc(129 * 8 * 1024);
	ConcurrentFree(p2, 129 * 8 * 1024);
}

使用定长内存池替代new

基本概念:定长内存池在申请内存时是直接向堆申请的,没有使用malloc,效率得到提升,而目前我们在本项目中用到到了很多new的操作,其本质还是malloc,因此我们要用定长内存池中的New()和Delete()函数来代替new和delete,进行内存申请和释放,从而提高程序执行效率 

准备工作:在某个涉及new或者delete的类中新增ObjectPool< ?>类型的私有成员变量,下面以PageCache为例

class PageCache
{
public:
    ...
private:
    ...
	ObjectPool<Span> _spanPool;//<>中的类型根据需要进行更改
    ...
};

替换方式:将PageCache.cpp中所有使用new的地方都换成_spanPool.New(),将所有使用delet的位置都换为_spanPool.Delete( ? )(?表示要删除的对象的名称可能是span也可能是kspan之类的)

//Span* kSpan = new Span;
Span* kSpan = _spanPool.New();

//delete span;
_spanPool.Delete(span);

易忽略位置

1、ConcurrentAlloc.h中的new ThreadCache

//pTLSThreadCache = new ThreadCache;
static ObjectPool<ThreadCache> tcPool;//static修饰保证只在当前文件中可以被访问
pTLSThreadCache = tcPool.New();

!!!重点!!! 

注意事项:pTLSThreadCache是每个线程独有的一个对象,但是为其申请空间的tcPool不是,它是一个静态的对象,整个进程中独一份,被当前进程中的所有线程共享,多线程情况下会出现线程安全问题,所以这里也需要加锁(不加的话有小概率不崩溃,即轮到t2执行时_memory不为空)

解决办法:在ObjPool类中新增公有成员变量_poolMtx,同时在pTLSThreadCache = tcPool.New()的两侧加锁

tcPool._poolMtx.lock();
pTLSThreadCache = tcPool.New();
tcPool._poolMtx.unlock();

补充:SpanList类中为了创建头结点的new Span不用替换,因为头节点通常在 SpanList 对象的整个生命周期内存在,并且不会像其他 Span 对象那样频繁创建和销毁。使用 _spanPool 进行内存管理主要是为了优化频繁分配和回收的对象,而头节点的长生命周期使得使用 _spanPool 的优势不明显

释放对象时不传对象大小

基本概念:在之前释放内存时我们不仅要传入释放的内存的地址,还要存放要释放的内存的大小,过于麻烦,所以最好只传递一个指针即可释放内存

ConcurrentFree(p1, 6);

补充内容1

在span类中新增一个表示当前span中管理的内存的大小的成员变量_objSize

struct Span
{
    ...
	size_t _objSize = 0;//当前span管理的内存大小
};

补充内容2

在ConcurrentFree中,将MapObjectToSpan的位置进行移动,并获取当前span的_objSize

//释放内存
static void ConcurrentFree(void* ptr)
{
	Span* span = PageCache::GetInstance()->MapObjectToSpan(ptr);
	size_t size = span->_objSize;
	if (size > MAX_BYTES)
	{
        ...
	}
	else
	{
        ...
	}
}

 补充内容3

在ConcurrentAlloc中从堆上获取到一个span后,补充该span的_objSize

//申请内存
static void* ConcurrentAlloc(size_t size)
{
    if (size > MAX_BYTES)
    {
        ...
    	Span* span = PageCache::GetInstance()->NewSpan(kpage);
	    span->_objSize = size;
        ...
    }
	else
	{
        ...
	}
}

补充内容4

CentralCache中的NewSpan后,填充该span的_objSize

//为指定位置桶下的SpanList申请一个非空的span
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{
    ...
	Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));//从PageCache中获取一个新的非空span
	span->_isUse = true;
	span->_objSize = size;

    ....
}

 注意事项:记得最后把ConcurrentFree的第二个形参删除

测试函数

void WithNoSize()
{
	void* p1 = ConcurrentAlloc(257 * 1024);
	ConcurrentFree(p1);
	
	void* p2 = ConcurrentAlloc(129 * 8 * 1024);
	ConcurrentFree(p2);
}

为哈希桶加锁

基本概念:C++的标准模板库(STL)提供的容器在多线程环境下并不保证线程安全,因此在多个线程同时访问或修改同一个容器时,通常需要自行添加同步机制(如互斥锁)以确保数据的一致性和避免竞态条件,因此当我们尝试在本项目中使用哈希桶记录span与页号的映射关系时,需要及时的加锁

加锁位置:参与读写哈希桶的函数有NewSpan、MapObjectToSpan和ReleaseSpanToPageCache,它们都在PageCache.cpp中,其中在CentralCache.cpp中使用这三个函数时,只有MapObjectToSpan没有添加锁,这就可能导致多个线程在CentralCache中同时访问MapObjectToSpan函数并同时访问哈希桶造成线程安全问题,所以要在MapObjectToSpan函数执行到访问哈希桶的操作前加锁

//地址->页号->span的映射
Span* PageCache::MapObjectToSpan(void* obj)
{
	size_t id = ((size_t)obj >> PAGE_SHIFT);
	std::unique_lock<std::mutex> lock(_pageMtx);

	auto ret = _idSpanMap.find(id);
	if (ret != _idSpanMap.end())
	{
		return ret->second;
	}
	else
	{
		assert(false);
		return nullptr;
	}
}

关于std::unique_lock<std::mutex> lock(_pageMtx):是一种通过RAII方式管理互斥锁的机制,确保在多线程环境中对共享资源的安全访问。它自动处理锁的获取和释放,减少了手动管理锁可能带来的错误风险,同时提供了较高的灵活性,适用于各种复杂的同步场景

多线程环境下对比malloc测试

新增Benchmark.cpp源文件

#include<cstdio>
#include<iostream>
#include<vector>
#include<thread>
#include<mutex>
#include"ConcurrentAlloc.h"
using namespace std;

void BenchmarkMalloc(size_t ntimes, size_t nworks, size_t rounds)//ntime一轮申请和释放内存的次数,round是跑多少轮,nworks是线程数
{
	std::vector<std::thread> vthread(nworks);
	std::atomic<size_t> malloc_costtime = 0;
	std::atomic<size_t> free_costtime = 0;
	for (size_t k = 0; k < nworks; ++k)
	{
		vthread[k] = std::thread([&, k]() {
			std::vector<void*> v;
			v.reserve(ntimes);
			for (size_t j = 0; j < rounds; ++j)
			{
				size_t begin1 = clock();
				for (size_t i = 0; i < ntimes; i++)
				{
					//v.push_back(malloc(16));
					v.push_back(malloc((16 + i) % 8192 + 1));
				}
				size_t end1 = clock();
				size_t begin2 = clock();
				for (size_t i = 0; i < ntimes; i++)
				{
					free(v[i]);
				}
				size_t end2 = clock();
				v.clear();
				malloc_costtime += (end1 - begin1);
				free_costtime += (end2 - begin2);
			}
			});
	}
	for (auto& t : vthread)
	{
		t.join();
	}
	printf("%u个线程并发执行%u轮次,每轮次malloc %u次: 花费:%u ms\n",
		nworks, rounds, ntimes, malloc_costtime.load());
	printf("%u个线程并发执行%u轮次,每轮次free %u次: 花费:%u ms\n",
		nworks, rounds, ntimes, free_costtime.load());
	printf("%u个线程并发malloc&free %u次,总计花费:%u ms\n",
		nworks, nworks * rounds * ntimes, malloc_costtime.load() + free_costtime.load());
}

// 单轮次申请释放次数 线程数 轮次
void BenchmarkConcurrentMalloc(size_t ntimes, size_t nworks, size_t rounds)
{
	std::vector<std::thread> vthread(nworks);
	std::atomic<size_t> malloc_costtime = 0;
	std::atomic<size_t> free_costtime = 0;
	for (size_t k = 0; k < nworks; ++k)
	{
		vthread[k] = std::thread([&]() {
			std::vector<void*> v;
			v.reserve(ntimes);
			for (size_t j = 0; j < rounds; ++j)
			{
				size_t begin1 = clock();
				for (size_t i = 0; i < ntimes; i++)
				{
					//v.push_back(ConcurrentAlloc(16));
					v.push_back(ConcurrentAlloc((16 + i) % 8192 + 1));
				}
				size_t end1 = clock();
				size_t begin2 = clock();
				for (size_t i = 0; i < ntimes; i++)
				{
					ConcurrentFree(v[i]);
				}
				size_t end2 = clock();
				v.clear();
				malloc_costtime += (end1 - begin1);
				free_costtime += (end2 - begin2);
			}
			});
	}
	for (auto& t : vthread)
	{
		t.join();
	}
	printf("%u个线程并发执行%u轮次,每轮次concurrent alloc %u次: 花费:%u ms\n",
		nworks, rounds, ntimes, malloc_costtime.load());
	printf("%u个线程并发执行%u轮次,每轮次concurrent dealloc %u次: 花费:%u ms\n",
		nworks, rounds, ntimes, free_costtime.load());
	printf("%u个线程并发concurrent alloc&dealloc %u次,总计花费:%u ms\n",
		nworks, nworks * rounds * ntimes, malloc_costtime.load() + free_costtime.load());
}
int main()
{
	size_t n = 10000;
	cout << "==========================================================" << endl;
	BenchmarkConcurrentMalloc(n, 10, 10);
	cout << endl << endl;
	BenchmarkMalloc(n, 10, 10);
	cout << "==========================================================" << endl;
	return 0;
}

性能瓶颈分析

针对性能瓶颈使用基数树进行优化

基数树代码

 ~over~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值