项目篇----仿tcmalloc的内存池设计(page cache)

1.引言

我们先梳理一下,当我们要申请x byte内存时,会先在thread cache中进行申请,如果申请到了就返回,申请不到就去central cache中申请,在central cache中,不会说你要x byte就给你x byte,它会通过慢启动以及一系列方法多给你开辟一些回去,剩下的就存到central cache中,那问题来了,如果central cache中也没有内存了咋办呢?此时就要向page cache申请,那么我们先来了解一下page cache的结构~

如下图所示,page cache也是哈希桶的映射结构,每个page上面会挂载着spanlist,但是要值得注意的是,当page cache从堆区申请回来一大块内存时,不是直接切好的,一下图为例,申请完的内存128page的span就挂在128那个桶里,假如我现在要2page的内存,就在这个128page的内存上切割,切成2page和126page,前者就被分配走了,让central cache拿走了,后者就挂在126page那个桶上了~

前文还说到,当usecount==0时,代表这个span已经空了,此时就会从central cache归还到span cache,那倘若现在又两个1page的内存块回来了,那page cache就会将这两个整合到一起,挂在2page那里,以此类推,如果central cache的所有的usecount都为0的话,那此时我的page cache是不是就算一块128page的大内存呀?当然前提是以这个图为例

所以我们在设计Page cache时,第一不要使用桶锁,因为这样这些Page会连续不断地开锁解锁,很降低性能,可以使用一个大锁!

其次就是还要用单例模式,原因同central cache,不多说了,类比参考一下。

这里补充说明申请和释放的流程,其实上面已经渗透的差不多了,看一下就好

2.内存分配

那这个page cache是给central cache分配几页内存呢?按心情分吗?显然不是,这里有这样两个函数,来看一下:有点像是极限法一样

复制代码
	static size_t NumMoveSize(size_t size)// 一次thread cache从中心缓存获取多少个
	{
		assert(size > 0);
		// [2, 512],一次批量移动多少个对象的(慢启动)上限值
		// 小对象一次批量上限高
		// 大对象一次批量上限低
		int num = MAX_BYTES / size; //计算:256KB能容纳多少个该size的对象
		if (num < 2)
			num = 2;        //分太少了一次不值得,下次还得申请,不如这次多给点
		if (num > 512)
			num = 512;     //分太多了也用不完,该浪费了
		return num;
	}

	// 计算一次向系统获取几个页
	// 单个对象 8byte
	// ...
	// 单个对象 256KB
	static size_t NumMovePage(size_t size)
	{
		size_t num = NumMoveSize(size);  //计算一次移动多少个对象
		size_t npage = num * size;       //计算这些对象总共需要多少字节
		npage >>= PAGE_SHIFT;     //转换成多少页数,按一页8KB=2^13来算
		if (npage == 0)
			npage = 1;
		return npage;

	}

NumMovePage(size) 计算的是:当 Thread Cache 向 Central Cache 申请对象,而 Central Cache 又需要向 Page Cache 申请内存时,应该一次性申请多少页

NumMoveSize 函数是 TCMalloc 中非常关键的批量策略算法,决定了每次从 Central Cache 移动多少个对象到 Thread Cache。

也许有人会好奇那我一次从page cache上面拿走了npage个页,那有可能用不了这些啊?那剩下的就会挂在central cache上对应的桶那里。

举个例子,我现在要申请8字节的内存,thread cache没有,申请到central cache,经过计算我要一次性带走512个,总共是512*8=4KB 的内存,那我带着这个数据就去找page cache,但是一页是8KB啊,但是我也只能拿走一页内存,不能从page cache中拿半页走,自己带4KB回到thread cache,剩下的4KB会留在Central Cache,挂在4KB那个桶里面,谁要谁来分!

3.单例模式

随后我们的page cache也要设计成单例模式,代码如下:

复制代码
#pragma once
#include"Common.h"
//单例模式
class PageCache
{
public:
	static PageCache* GetInstance()
	{
		return &_sInst;
	}
	Span* NewSpan(size_t k);
	std::mutex _pageMX;
private:
	SpanList _spanList[NPAGES];
	PageCache()
	{}

	PageCache(const PageCache&) = delete;
	static PageCache _sInst;
};

4.内存切割

当我们向取K页内存时,可以这样做:

先尝试现成的 ,没有就切分大的 ,再没有就批发128页 ,然后假装这128页本来就在那,重新走流程。具体代码有详解:

复制代码
#include"PageCache.h"

PageCache PageCache::_sInst;

//获取一个K页的span
Span* PageCache::NewSpan(size_t k)
{
	assert(k > 0 && k < NPAGES);
	// 先检查第k个桶里面有没有span

	if (!_spanList[k].Empty())
	{
		return _spanList->PopFront();
	}

	//到这里就说明为空了
	// 检查一下后面的桶里面有没有span,如果有可以把他它进行切分
	for (size_t i = k + 1; i < NPAGES; i++)
	{
		if (!_spanList[i].Empty())
		{
			//切下来一块
			Span* nspan = _spanList[i].PopFront();
			Span* kspan = new Span;

			// 在nSpan的头部切一个k页下来
			// k页span返回
			// nSpan再挂到对应映射的位置
			//下面不太好理解,举个租房子的例子,Span 就是"租赁合同",_pageId = 从哪间房开始租,起始位置
			//	_n = 连续租了几间房,
			kspan->_pageId = nspan->_pageId;  //从头开始拿
			kspan->_n= k;					//拿k间

			nspan->_pageId += k;			//剩下的房间开始位置
			nspan->_n -= k;					//剩下连续房间的数量

			_spanList[nspan->_n].PushFront(nspan);

			return kspan;
		}
	}

	// 走到这个位置就说明后面没有大页的span了
	// 这时就去找堆要一个128页的span
	Span* bigspan = new Span;
	void* ptr = SystemAlloc(NPAGES - 1);
	bigspan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;

	bigspan->_n = NPAGES - 1;

	_spanList[bigspan->_n].PushFront(bigspan);
	return NewSpan(k);
}

5.central cache获取span

先查缓存,无则解锁申请,无锁切割,加锁挂链,用最少的锁实现高并发内存分配。

复制代码
// 获取一个非空的span
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{
	// 查看当前的spanlist中是否有还有未分配对象的span
	//遍历链表
	Span* it = list.Begin();
	while (it != list.End())
	{
		if (it->_freelist != nullptr)
		{
			return it;
		}
		else
		{
			it = it->_next;
		}
	}
	//走到这说明没有未分配对象的span
	// 先把central cache的桶锁解掉,这样如果其他线程释放内存对象回来,不会阻塞
	list._mtx.unlock();

	//只能管page cache要了
	PageCache::GetInstance()->_pageMX.lock();
	Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));
	PageCache::GetInstance()->_pageMX.unlock();

	//现在申请完了是一块大块的内存,下面进行切割,不需要加锁,因为这会其他线程访问不到这个span
	// 计算span的大块内存的起始地址和大块内存的大小(字节数)
	char* start = (char*)(span->_pageId << PAGE_SHIFT);
	size_t bytes = span->_n << PAGE_SHIFT;
	char* end = start + bytes;
	// 把大块内存切成自由链表链接起来
	// 1、先切一块下来去做头,方便尾插
	span->_freelist = start;
	start += size;
	void* tail = span->_freelist;
	int i = 0;
	while (start < end)
	{
		i++;
		NextObj(tail) = start;
		tail =NextObj(tail);
		start += size;
	}
	// 切好span以后,需要把span挂到桶里面去的时候,再加锁
	list._mtx.lock();
	list.PushFront(span);
	return span;
}

6.单元测试

复制代码
void TestConcurrentAlloc1()
{
	void* p1 = ConcurrentAlloc(6);
	void* p2 = ConcurrentAlloc(8);
	void* p3 = ConcurrentAlloc(1);
	void* p4 = ConcurrentAlloc(7);
	void* p5 = ConcurrentAlloc(8);

	cout << p1 << endl;
	cout << p2 << endl;
	cout << p3 << endl;
	cout << p4 << endl;
	cout << p5 << endl;
}

int main()
{
	TestConcurrentAlloc1();
	return 0;
}

结果:

相关推荐
m0_706653232 小时前
C++中的解释器模式
开发语言·c++·算法
王老师青少年编程2 小时前
2022信奥赛C++提高组csp-s复赛真题及题解:数据传输
c++·数据传输·真题·csp·信奥赛·csp-s·提高组
hetao17338372 小时前
2026-01-29~02-03 hetao1733837 的刷题记录
c++·笔记·算法
晚风吹长发2 小时前
初步了解Linux中的POSIX信号量及环形队列的CP模型
linux·运维·服务器·数据结构·c++·算法
近津薪荼2 小时前
优选算法——前缀和(1):一维前缀和
c++·学习·算法
草莓熊Lotso4 小时前
Linux 基础 IO 初步解析:从 C 库函数到系统调用,理解文件操作本质
linux·运维·服务器·c语言·数据库·c++·人工智能
闻缺陷则喜何志丹4 小时前
P8699 [蓝桥杯 2019 国 B] 排列数|普及+
c++·数学·蓝桥杯·数论·洛谷·数列
D_evil__10 小时前
【Effective Modern C++】第三章 转向现代C++:16. 让const成员函数线程安全
c++
Queenie_Charlie11 小时前
前缀和的前缀和
数据结构·c++·树状数组