高并发内存池实现

1.基础知识

内存池的意义:

**1.**通过提前申请一块足够大的内存空间,从而减少直接与内存的频繁交互,达到提高内存申请使用效率的目的

2.只使用这一块足够大的内存空间,减少系统内存空间中的内存碎片化(外碎片)

2.定长内存池实现

特点:固定大小的内存申请释放

优点:

1.性能达到极致

2.不用考虑内存碎片问题

头文件实现:

cpp 复制代码
#pragma once
#include<iostream>
using std::cout;
using std::cin;
using std::endl;
template <class T>
class ObjectPool
{
public:
	T* New()
	{
		T* obj = nullptr;
		if (_freelist)
		{
			void* next = *(void**)_freelist;
			obj = _freelist;
			_freelist = next;
		}
		else
		{
			if (_remainBytes < sizeof(T))//内存池剩余空间不足时重新申请大块空间
			{
				_memory = (char*)malloc(128 * 1024);
				if (_memory == nullptr)
				{
					throw std::bad_alloc();
				}
				_remainBytes = 128 * 1024;
			}
			T* obj = (T*)_memory;
			_memory += sizeof(T);//指向内存池可存储空间的指针移动
			_remainBytes -= sizeof(T);
		}
		new(obj)T;//定位new,显式调用构造
		return obj;
	}
	void Delete(T* obj)
	{
		//显式析构
		obj->~T();
		//freelist为空或不为空头插方式处理逻辑兼容
		*(void**)obj = _freelist;
		_freelist = obj;
	}
private:
	char* _memory = nullptr;//用char*方便内存切割,是指向大块内存的指针
	size_t _remainBytes = 0;//大块内存剩下的空间所占字节数
	void* _freelist = nullptr;//回收链表
};

(1)框架设计:

由于是定长内存池,所以这里我们使用模板类型对长度进行设计,从而可以适应各种长度的类型(主要是自定义类型)

总体过程就是从内存池中逐块的获取定长内存,然后用完交给回收链表,回收链表的内存可以重复利用,当回收链表和内存池的空间都不足的时候再从系统申请空间给到内存池

疑问1:回收链表中节点是如何知道下一个归还空间的地址的?

我们可以在定长空间的前段存储下一个归还空间的地址

疑问2:在32位和64位操作系统中,指针的占用字节数分别为4/8,我们要怎么控制前段空间大小?

我们可以使用*(void **)

对void**解引用的含义就是打开void*类型占用的字节空间,而void*在32位下是4字节,在64位下是8字节,完美的解决了该问题

(2)成员变量:

指向内存池空位置的指针:_memory(设置为char*类型,方便进行指针移动)

指向回收链表头节点的指针:_freeList

内存池剩余的空间所占字节数:_remainBytes

(3)申请:

返回值T*:需要的是可以容纳T类型数据的空间,所以返回指向T类型指针

当回收链表有空间时,先使用回收链表的空间,若没有则判断内存池是否有空间,有就使用,没有就再从堆空间申请

为了兼容T是自定义类型,我们还需要利用定位new显式调用构造函数

(4)释放:

在释放的时候,如果我们采用空闲节点头插的形式插入回收链表,那么无论回收链表是否为空,逻辑都兼容

3.高并发内存池

3.1框架

(1)thread cache:线程独享内存空间(256kb,指的是单块内存上限),每个线程占用一个,不需要加锁

这也是并发高效的地方

对应的桶后面链接的是对应大小的空间

(2)central cache:中心缓存,所有线程共享,需要加桶锁。当thread cache的内存不够时,就按需从central cache中申请空间,用完之后看情况归还空间

中心缓存是用于给threadcache进行内存空间补充的,且他是所有线程共用的一个对象,需要加锁,且中心缓存的映射规则和threadcache完全一致

**加桶锁:**为了尽量提高运行效率,我们不增加中心缓存对象锁,而是对每个中心缓存的桶分别加锁

**span:**这是以页为单位的大块内存,会提前根据当前桶的映射字节数对span进行分割,在threadcache需要若干对象空间是,返回若干块分割好的空间(减少threadcache申请次数提高效率)

**回收空间:**如果threadcache用完了空间,有多余的需要根据情况归还centralcache,从而可以让其他线程也能使用空间,完成负载均衡

(3)page cache:页缓存会在中心缓存内存不足时进行内存补充,且可以解决内存碎片问题

当centralcache对应桶没有span对象空间时,向pagecache中申请若干页page,此时pagecache会检查对应桶是否有span,如果没有就找page数更大的桶的span,找到之后将其分割成我们需要的大小的span

eg:我们需要5page的span,找到10page的span后将其分割成两个5page的span,一个返回,一个连入5page的桶中

映射规则:

threadcache和centralcache的桶映射规则都是一样的,而pagecache的映射规则则不一样

他是根据span空间拥有的page数来划分的,索引为1的桶表示它的span都是具有一页连续空间的span

释放内存:

centralcache返回的span我们可以从小到大寻找pageid,看看是否可以合并成更大的span,如果成功合并就继续寻找更大的桶内部的span,以此类推,达到减少内存碎片的目的

锁的使用:

高并发内存池中,小对象缓存(如 central cache)使用桶锁 是为了分散高频竞争,而page cache 不使用桶锁,是因为其访问模式、操作特性和竞争压力都更适合粗粒度锁,引入桶锁会导致复杂度上升而收益有限

注意:

内存管理中一页的大小是4kb(管理内存的真实单位)

MySQL中一页的大小是16kb

高并发内存池中一页规定为8kb是抽象出来的基于设计效率的设置


项目流程:

当用户单次申请的空间大小小于256kb时

用户申请层与ThreadCache层交互

(1)进行小于256kb的内存申请

(2)将获取到的对象链入ThreadCache的freelist中

ThreadCache层与CentralCache层交互

(3)ThreadCache层的freelist中没有对应大小的对象,向CentralCache层申请若干对象

(4)freelist的对应桶对象数超过MAX_SIZE,将MAX_SIZE数量的对象归还CentralCache层的对应span

CentralCache层与PageCache层交互

(5)CentralCache中的spanlist对应桶中没有空闲的span,向PageCache申请若干页大小的span

(6)当前归还的span若use_count归零,先合并前后pageid的页,然后归还span给pagecache

PageCache层与堆交互

(7)当pagecache中也没有所需大小的span,向堆中申请128page大小的span,并挂载到128page的桶中,进行后续切割,若足够就切割当前已有的span并返回

当用户单次申请空间大小大于256kb时

(8)(9)(10):用户直接通过pagecache层的接口,间接向堆申请空间,并且在使用完毕后,通过pagecache层的接口释放空间到堆中


ThreadCache层

CentralCache层

PageCache层

用户获取唯一的ThreadCache对象

对应步骤的相关函数:

(1)

static void* ConcurrentAlloc(size_t size);

void* Allocate(size_t size);

(2)

static void ConcurrentFree(void* ptr)

void Deallocate(void* ptr, size_t size);

(3)

void* FetchFromCentralCache(size_t index, size_t size);(上层封装)

size_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size);(具体实现)

(4)

void ListTooLong(FreeList & list, size_t size);

(5)

Span* GetOneSpan(SpanList& list, size_t size);

(6)

void ReleaseListToSpans(void* start, size_t size);

Span * MapObjectToSpan(void* obj);

void ReleaseSpanToPageCache(Span * span);

(7)

Span* NewSpan(size_t k);

(8)(9)(10)

Span* NewSpan(size_t k);(获取空间)

void ReleaseSpanToPageCache(Span * span);(释放空间)

通用类:

FreeList,SizeClass,Span, SpanList

3.2通用类

(1)FreeList

cpp 复制代码
//管理切分好的小对象的自由链表
void*& Nextobj(void* obj)//获取当前节点存储下一个节点地址的空间
{
	return *(void**)obj;
}
class FreeList
{
public:
	void Push(void* obj)//头插
	{
		Nextobj(obj) = _freelist;
		_freelist = obj;
		_size++;
	}
	void PushRange(void* start, void* end, size_t num)//多段插入
	{
		Nextobj(end) = _freelist;
		_freelist = start;
		_size += num;
	}
	void PopRange(void*& start, void*& end, size_t num)
	{
		assert(num >= _size);//全量回收(回收maxsize块)
		end = start;
		for (int i = 0; i < num - 1; i++)
		{
			end = Nextobj(end);
		}
		_freelist = Nextobj(end);
		Nextobj(end) = nullptr;
		_size -= num;
	}
	void* Pop()//头删
	{
		assert(_freelist);
		void* obj = _freelist;
		_freelist = Nextobj(obj);
		_size--;
		return obj;
	}
	bool Empty()
	{
		return _freelist == nullptr;
	}
	size_t& MaxSize()//引用返回方便++
	{
		return _maxSize;
	}
	size_t Size()
	{
		return _size;
	}
private:
	void* _freelist = nullptr;
	size_t _maxSize = 1;//桶最大容量上限
	size_t _size = 0;//当前桶的节点个数
};

**功能:**freelist是用于管理ThreadCache层所拥有的对象空间的自由链表,当线程单次需要申请小于256kb大小的空间时,从该自由链表中查询获取。当线程使用完空间归还的时候,将对应对象链入freelist相应索引位置

FreeList类描述的是单条自由链表,而自由链表数组才是ThreadCache管理对象空间的整体

前置关联函数:Nextobj

由于计算机可能处于32位或64位操作系统下,他们的指针大小分别是4字节和8字节,为了精确获取到对象空间中存储的同一个桶中下一个对象的地址,我们需要确保在32位下获取4字节,在64位下获取8字节

而因为void*表示的是当前操作系统的指针字节数,我们可以解引用void**类型的obj来获取到obj的前4/8字节空间信息,将其作为地址返回

图示:

、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、。。

成员变量:_freelist指向当前桶的头结点,_maxSize表示当前桶的容量上限(根据慢反馈调节算法不断++),_size当前桶中对象节点个数

成员函数:和链表类似的实现,头插头删就不解释了

PushRange():多段插入,将从CentralCache申请到的对象一并链入桶中

PopRange():多段删除,当桶中数据超过maxsize时,将maxsize个对象一并提取出来,结合后续接口,将对象返回给CentralCache

(2)SizeClass

cpp 复制代码
class SizeClass
{
public:
//为了减少自由链表的个数,且控制内碎片的占比,我们有如下对齐规则
// [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)
	static inline size_t _RoundUp(size_t size, size_t AlignNum)//方法二
	{
		return (size + AlignNum - 1) & ~(AlignNum - 1);//前半段确保对齐数的位数一定始终为1,后半段将不足对齐数的数据全部清除
	}
	static 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);
		}
	}
	//计算具体对应哪个桶
	static inline size_t _Index(size_t size, size_t align_shift)//参数二:对齐数二进制位数
	{
		return ((size + (1 << align_shift) - 1) >> align_shift) - 1;//左段先将映射范围内的数据都增大到超过他们本身对齐数
		//但是不足下一级,然后除他们的对齐数,从而得到他们所处的桶是第几个,最后减一得到数组下标
	}
	// 计算映射的哪一个自由链表桶
	static inline size_t Index(size_t size)
	{
		assert(size <= MAX_BYTES);

		// 每个区间有多少个链
		static int group_array[4] = { 16, 56, 56, 56 };
		if (size <= 128) {
			return _Index(size, 3);
		}
		else if (size <= 1024) {
			return _Index(size - 128, 4) + group_array[0];
		}
		else if (size <= 8 * 1024) {
			return _Index(size - 1024, 7) + group_array[1] + group_array[0];
		}
		else if (size <= 64 * 1024) {
			return _Index(size - 8 * 1024, 10) + group_array[2] + group_array[1] + group_array[0];
		}
		else if (size <= 256 * 1024) {
			return _Index(size - 64 * 1024, 13) + group_array[3] + group_array[2] + group_array[1] + group_array[0];
		}
		else {
			assert(false);
		}
		return -1;
	}
	// threadcache一次从中心缓存获取最多多少个对象(threadcache->centralcache)
	static size_t NumMoveSize(size_t size)
	{
		if (size == 0)
			return 0;

		// [2, 512],一次批量移动多少个对象的(慢启动)上限值
		int num = MAX_BYTES / size;
		if (num < 2)
			num = 2;

		if (num > 512)
			num = 512;

		return num;
	}
	// 计算一次向系统申请的页数量()
	//size表示单个对象大小
	static size_t NumMovePage(size_t size)
	{
		size_t num = NumMoveSize(size);    // 获取批量移动的对象数量
		size_t npage = num * size;         // 计算总字节数
		npage >>= PAGE_SHIFT;              // 字节数转页数(等价于除以2^12)

		if (npage == 0)                    // 确保至少申请1页
			npage = 1;

		return npage;
	}
};

功能:提供size的映射桶方法,适用于threadcache和centralcache层映射

由于我们申请内存的时候只能获取到需要申请的空间大小size,为了让他能够在freelist中正确找到对应空间大小的对象,我们还需要将他的size对齐,并找到freelist中他实际映射的桶索引

映射规则在注释中标注,没有全部按照8对齐,是为了减少桶链的个数

设计风格:计算处理函数和表面函数分离,降低耦合度

1.size对齐

由于size所处的范围不同,我们的对齐数也会不同,所以在RoundUp函数中,我们使用条件语句进行分区,分别使用不同的对齐数进行映射

映射函数:_RoundUp,参数一为需要申请的单块空间大小,参数二为对齐数

(size + AlignNum - 1) & ~(AlignNum - 1)

&前段的作用是确保该数的对齐数对应二进制位一定为1,然后该范围内的不同的数的二进制区别只有对齐数二进制位的后续位从0~1不断变化

&后段的作用是将对齐数对应二进制位后续位全部变为0,从而与前段进行&操作的时候,所有该范围内的数最终都会将后续二进制位变为0,只保留对齐数对应二进制位,对齐成功

eg:1~8的数,对齐数为8

1:(1+8-1)& ~7

1000 & 1000 = 1000 (十进制变为8)

2:(2+8-1) & ~7

1001 & 1000 = 1000 (十进制为8)

..........

2.查询索引

我们先计算出不同分段的桶的个数,然后按照不同的对齐数分别计算在对应对齐数分段内映射的索引位置,最后再加上前一个对齐数分段的桶个数

索引函数:_Index,参数有申请空间大小size,和对齐数位移位数

((size + (1 << align_shift) - 1) >> align_shift) - 1

这里采用了不一样的处理方法,通过1<<align_shift也可以得到对齐数。

我们先将范围内的数增大到>=对齐数且<=下一个对齐数,然后除对齐数,得到该数所处当前范围的第几个桶,再减一得到下标

注意:后续的Index函数中,我们不仅要先size-前段对齐数(每段的对齐数不一样,计算参数就不同,需要减去),还要将前段的总桶数加上

3.对象申请上限计算与页申请上限计算

对象申请上限,直接用可申请最大字节数MAX_BYTES(256*1024)除需申请的单个对象字节对齐数,然后对过高和过低做调整,保证不会申请过多或过少

页申请上限计算,先计算出总申请字节数然后除8*1024(以8kb为一页),然后控制至少申请一页

(3)Span

cpp 复制代码
//管理多个连续页大块内存结构
struct Span
{
	PAGE_ID _pageId = 0;//起始页的页号
	size_t _n = 0;//页数量
	//双向链表指针
	Span* _next = nullptr;
	Span* _prev = nullptr;
	
	size_t _useCount = 0;//小块内存被使用数
	size_t _objsize = 0;//切好的小对象大小
	void* _freelist = nullptr;//切好的小块内存的自由链表
	bool _isuse = false;//Span是否被分配给centralcache使用
};

**功能:**管理单个span对象,同时span也是处于spanlist中的

(4)SpanList

cpp 复制代码
//带头双向循环链表
class SpanList
{
public:
	SpanList()
	{
		_head = new Span;
		_head->_next = _head;
		_head->_prev = _head;
	}
	Span* Begin()
	{
		return _head->_next;
	}
	Span* End()
	{
		return _head;
	}
	void PushFront(Span* span)
	{
		Insert(Begin(), span);
	}
	Span* PopFront()
	{
		Span* front = _head->_next;
		Erase(front);
		return front;
	}
	void* Insert(Span* pos, Span* newspan)
	{
		assert(pos);
		assert(newspan);
		Span* prev = pos->_prev;
		//prev newspan pos
		prev->_next = newspan;
		newspan->_prev = prev;
		newspan->_next = pos;
		pos->_prev = newspan;
	}
	void* Erase(Span* pos)
	{
		assert(pos);
		assert(pos != _head);
		Span* prev = pos->_prev;
		Span* next = pos->_next;
		prev->_next = next;
		next->_prev = prev;
	}
	bool Empty()
	{
		return _head == nullptr;
	}
private:
	Span* _head;
public:
	std::mutex _mtx;//桶锁
};

**功能:**管理span的单桶

成员变量:_head,指向当前桶的头节点(不存储实际数据);

_mtx用于给桶加锁,因为spanlist是用于centralcache和pagecache中的,所有线程共享

**实现:**都是双向链表基本操作

3.3用户申请层与ThreadCache层交互

(1)进行小于256kb的内存申请

cpp 复制代码
void* ThreadCache::Allocate(size_t size)
{
	assert(size <= MAX_BYTES);
	size_t alignsize = SizeClass::RoundUp(size);
	size_t index = SizeClass::Index(size);
	if(!_freelist[index].Empty())
	{
		return _freelist[index].Pop();
	}
	else
	{
		return FetchFromCentralCache(index, alignsize);
	}
}

申请前置准备:根据当前需求空间大小size得出映射桶代表的字节数,根据size得出桶索引

(前置步骤是为了给下一层传参的)

如果对应桶中存在对象,那么就将对象pop并返回

若不存在对象,往下一层CentrealCache申请

cpp 复制代码
//每个线程通过TLS无锁获取对应threadcache对象
static void* ConcurrentAlloc(size_t size)
{
	if (size > MAX_BYTES)//超过256kb
	{
		size_t alignsize = SizeClass::RoundUp(size);
		size_t kpage = alignsize >> PAGE_SHIFT;

		PageCache::GetInstance()->_pagemtx.lock();
		Span* span = PageCache::GetInstance()->NewSpan(kpage);
		span->_objsize = size;
		PageCache::GetInstance()->_pagemtx.unlock();

		void* ptr = (void*)(span->_pageId << PAGE_SHIFT);
		return ptr;
	}
	else
	{
		if (pTLSThreadCache == nullptr)
		{
			static ObjectPool<ThreadCache> tcpool;
			pTLSThreadCache = tcpool.New();
		}
		return pTLSThreadCache->Allocate(size);
	}
}

**线程局部存储(TLS):**这是一个变量存储方法,可以让该变量对于该线程全局访问,同时其他线程无法访问,从而避免设置全局变量的加锁操作

ThreadCache.h中

cpp 复制代码
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;//设置当前可见用static

**1.申请空间小于256kb:**若当前线程的pTLSThreadCache为空,说明他的ThreadCache对象还没有创建,我们就new一个对象,然后申请空间返回,释放也是调用ThreadCache对象的类内成员函数

**2.申请空间大于256kb:**先计算出有kpage页的申请空间,经过加锁之后,调用NewSpan从堆中申请空间,成功后将pageid转换为指针返回

(2)将获取到的对象链入ThreadCache的freelist中

cpp 复制代码
void ThreadCache::Deallocate(void* ptr, size_t size)
{
	assert(size <= MAX_BYTES);
	assert(ptr);
	//找出映射的桶,将对象头插归还自由链表
	size_t index = SizeClass::Index(size);
	_freelist[index].Push(ptr);
	//当前桶过长,回收maxsize对象到centralcache
	if (_freelist[index].Size() >= _freelist[index].MaxSize())
	{
		ListTooLong(_freelist[index], size);
	}
}

找出对应的桶,将归还对象插入自由链表,若当前自由链表的长度超过最大长度,归还MAX_SZIE个对象给CentralCache

cpp 复制代码
static void ConcurrentFree(void* ptr)
{
	Span* span = PageCache::GetInstance()->MapObjectToSpan(ptr);
	size_t size = span->_objsize;
	if (size > MAX_BYTES)
	{

		PageCache::GetInstance()->_pagemtx.lock();
		PageCache::GetInstance()->ReleaseSpanToPageCache(span);
		PageCache::GetInstance()->_pagemtx.unlock();

	}
	else
	{
		assert(pTLSThreadCache);
		pTLSThreadCache->Deallocate(ptr, size);
	}
}

当申请大于256就调用pagecache层的接口,将空间归还堆,若小于256kb就调用deallocate释放到自由链表

3.4ThreadCache层与CentralCache层交互

(3)ThreadCache层的freelist中没有对应大小的对象,向CentralCache层申请若干对象

cpp 复制代码
void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{
	//慢开始反馈调节算法->确认一次移动的对象个数
	//该算法会根据该桶的需求次数来不断增加请求个数
	size_t batchNum = min(_freelist[index].MaxSize(), SizeClass::NumMoveSize(size));
	if (_freelist[index].MaxSize() == batchNum)
	{
		_freelist[index].MaxSize() += 1;
	}
	//链表头尾
	void* start = nullptr;
	void* end = nullptr;
	size_t actualnum = CentralCache::GetInstance()->FetchRangeObj(start,end,batchNum,size);
	assert(actualnum >= 1);
	if (actualnum == 1)//只获取了一个对象空间
	{
		assert(start == end);
		return start;
	}
	else//获取了多个对象空间
	{
		//剩余对象连入freelist
		_freelist[index].PushRange(Nextobj(start), end,actualnum-1);
		return start;
	}
	return nullptr;
}

**慢反馈调节算法:**控制一开始申请的对象数从1开始,然后根据申请次数逐渐增加,上限由SizeClass中的NumMoveSize来控制

确认需要申请的对象个数后,调用FetchRangeObj从centralcache中拿对象链表

申请失败

返回nullptr

申请成功

若最终申请的对象恰好为1,直接返回,否则将除了第一个对象外的对象链入freelist中,然后返回start指针

cpp 复制代码
// 从中心缓存获取一定数量的对象给thread cache
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size)
{
	size_t index = SizeClass::Index(size);
	_spanlists[index]._mtx.lock();//对应桶加桶锁
	//获取非空span
	Span* span = GetOneSpan(_spanlists[index], size);
	assert(span);
	assert(span->_freelist);
	//将获取到的对象链表头尾指针设置好
	start = span->_freelist;
	end = start;
	size_t realnum = 1;//实际从span获取到的对象数
	for (int i = 1; i <= batchNum - 1 && Nextobj(end) != nullptr; i++)
	{
		end = Nextobj(end);
		realnum++;
	}
	span->_freelist = Nextobj(end);
	Nextobj(end) = nullptr;
	span->_useCount += realnum;
	_spanlists[index]._mtx.unlock();
	return realnum;
}

整体流程:

确认索引index,调用GetOneSpan获取一个非空span,利用循环将申请到的非空span的真实对象数以及end地址遍历出来,最后将申请到的span从span->_freelist中取出来,返回真实申请到的对象数

细节:
**1.加桶锁:**由于centralcache是单例模式,所有线程共享,所以我们需要加锁,可是加整层的锁会出现锁竞争导致的效率问题,所以我们对每个桶进行加锁

**2.realnum:**由于我们申请span的时候不一定能够完全按照我们所想来获取足够对象,所以申请完毕之后还需要统计真实对象数

(4)freelist的对应桶对象数超过MAX_SIZE,将MAX_SIZE数量的对象归还CentralCache层的对应span

cpp 复制代码
// 释放对象时,链表过⻓时,回收内存回到中⼼缓存
void ThreadCache::ListTooLong(FreeList& list, size_t size)
{
	void* start = nullptr;
	void* end = nullptr;
	list.PopRange(start,end,list.MaxSize());//回收maxsize块内存

	//归还到中心缓存层
	CentralCache::GetInstance()->ReleaseListToSpans(start, size);
}

调用PopRange将freelist中的前MAX_SIZE个对象提取出来,然后调用ReleaseListSpans将list归还到centralcache

3.5CentralCache层与PageCache层交互

(5)CentralCache中的spanlist对应桶中没有空闲的span,向PageCache申请若干页大小的span

cpp 复制代码
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{
	//遍历所有centralcache的桶,查找空闲span
	Span* it = list.Begin();
	while (it != list.End())
	{
		if (it->_freelist != nullptr)
		{
			return it;
		}
		else
		{
			it = it->_next;
		}
	}
	//将centralcache的桶锁先解除,方便其他线程的申请释放不卡住
	list._mtx.unlock();

	//从pagecache申请span
	PageCache::GetInstance()->_pagemtx.lock();//加pagecache锁
	Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));
	span->_isuse = true;
	span->_objsize = size;
	PageCache::GetInstance()->_pagemtx.unlock();//解pagecache锁

	// (pageid是系统分配的,id为0就表示0号地址(初始地址))
	//通过页号计算起始地址并计算大块内存总字节数
	char* start = (char*)(span->_pageId << PAGE_SHIFT);
	size_t bytes = span->_n << PAGE_SHIFT;
	char* end = start + bytes;

	//大块内存切小用自由链表链接起来(使用尾插,让内存地址连续)
	//先切一个空间做头结点,方便尾插
	span->_freelist = start;
	start += size;
	void* tail = span->_freelist;
	//持续切割
	while (start < end)
	{
		Nextobj(tail) = start;
		tail = start;
		start += size;
	}
	Nextobj(tail) = nullptr;
	//将新申请的空间头插链接到centralcache的自由链表
	list._mtx.lock();//对桶进行插入span,需要加锁
	list.PushFront(span);
	return nullptr;
}

**流程:**遍历centralcache中的所有桶,查找空闲span,如果有就直接返回对应span的地址

若没有则向pagecache层申请一块页数足够的span,然后将大块内存切割成若干个size大小的空间,尾插链入span的freelist中,最终链入centralcache的对应桶中

细节:
1.解除与添加centralcache的桶锁:

当我们需要从pagecache中申请span时,由于暂时不需要使用centralcache的桶,所以我们直接解除centralcache的桶锁

当我们需要将申请到的span交给centralcache的桶,需要加桶锁

2.获取span的首尾地址:

首地址可以通过span内部的pageid计算,因为pageid是系统分配的id为0表示0号地址,所以pageid乘一页字节大小就可以得到当前页的地址

尾地址可以通过求出span总占用字节然后从首地址+bytes求的

3.切割时插入方式:

使用尾插可以保持span的freelist中内存地址连续

(6)从ThreadCache归还对象给CentralCache,若当前归还的对象所处span的use_count归零,先合并前后pageid的页,然后归还span给pagecache

cpp 复制代码
// 将⼀定数量的对象释放到span跨度
void CentralCache::ReleaseListToSpans(void* start, size_t size)
{
	size_t index = SizeClass::Index(size);
	_spanlists[index]._mtx.lock();
	while (start)
	{
		void* next = Nextobj(start);
		Span* span = PageCache::GetInstance()->MapObjectToSpan(start);//找到对应页的span地址
		Nextobj(start) = span->_freelist;
		span->_freelist = start;
		span->_useCount--;
		if (span->_useCount == 0)//该span的切割对象全部回收,格式化后返回给pagecache
		{
			_spanlists[index].Erase(span);
			span->_freelist = nullptr;
			span->_next = nullptr;
			span->_prev = nullptr;
			_spanlists[index]._mtx.unlock();

			PageCache::GetInstance()->_pagemtx.lock();
			PageCache::GetInstance()->ReleaseSpanToPageCache(span);

			_spanlists[index]._mtx.lock();
		}
		start = next;
	}
	_spanlists[index]._mtx.unlock();
}

遍历需要归还的所有对象,并根据MapObjectToSpan接口找到对象对应的span,将当前对象从原始链表链入其所属span的freelist中,并将span的usecount--,表示使用的span内切割对象又减少一个

**当usecount归零,归还给pagecache:**先将归零span在centralcache的spanlists中删除,并置空span的相关成员变量值,最后调用归还pagecache的接口

cpp 复制代码
// 获取从对象到span的映射(指针-》页号-》span地址)
Span* PageCache::MapObjectToSpan(void* obj)
{
	PAGE_ID id = ((PAGE_ID)obj >> PAGE_SHIFT);
	std::unique_lock<std::mutex> lock(_pagemtx);//RAII风格加锁
	auto ret = _idspanmap.find(id);
	if (ret == _idspanmap.end())
	{
		assert(false);
		return nullptr;
	}
	else
	{
		return ret->second;
	}
}

该接口主要是用于找到对象和span的映射

首先地址可以通过计算得到pageid,然后我们的pageid和span之间具有哈希映射

( std::unordered_map<PAGE_ID, Span*> _idspanmap;//建立页号和span的映射关系),从而让对象地址和span可以关联起来

cpp 复制代码
// 释放空闲span回到Pagecache,并合并相邻的span
void PageCache::ReleaseSpanToPageCache(Span* span)
{
	//大于128页的向堆申请的空间释放
	if (span->_n > NPAGES - 1)
	{
		void* ptr = (void*)(span->_pageId << PAGE_SHIFT);
		SystemFree(ptr);
		/*delete span;*/
		_spanpool.Delete(span);
		return;
	}
	//对span的前后页号的页尝试合并,缓解外碎片问题
	 //前向合并
	while (1)
	{
		PAGE_ID previd = span->_pageId - 1;
		auto ret = _idspanmap.find(previd);
		//前面没有页,不合并
		if (ret == _idspanmap.end())
		{
			break;
		}
		//前面的页的span正在使用,不合并
		Span* prevspan = ret->second;
		if (prevspan->_isuse == true)
		{
			break;
		}
		//合并后超过128管理上限,不合并
		if (span->_n + prevspan->_n > NPAGES - 1)
		{
			break;
		}
		//合并
		span->_pageId = prevspan->_pageId;
		span->_n += prevspan->_n;
		//将合并掉的span从list中删除
		_spanlists[prevspan->_n].Erase(prevspan);
		//delete prevspan;//释放对象
		_spanpool.Delete(prevspan);
	}
	//后向合并
	while (1)
	{
		PAGE_ID nextid = span->_pageId + span->_n;
		auto ret = _idspanmap.find(nextid);
		if (ret == _idspanmap.end())
		{
			break;
		}
		Span* nextspan = ret->second;
		if (nextspan->_isuse == true)
		{
			break;
		}
		if (nextspan->_n + span->_n > NPAGES - 1)
		{
			break;
		}
		//合并
		span->_n += nextspan->_n;
		//将合并掉的span从list中删除
		_spanlists[nextspan->_n].Erase(nextspan);
		/*delete nextspan;*/
		_spanpool.Delete(nextspan);
	}
	//挂载到spanlists
	_spanlists[span->_n].PushFront(span);
	span->_isuse = false;
	//保存首尾pageid的映射
	_idspanmap[span->_pageId] = span;
	_idspanmap[span->_pageId + span->_n - 1] = span;
}

先忽略第一个if语句内容,那是线程申请的单次空间大于256kb的情况处理

在归还到pagecache之前,我们尝试前向合并和后向合并

前向合并:

1.若previd不存在:不合并

2.若previd的span正在使用:不合并

3.若previd的span和当前span合并之后大于128page:不合并

开始前向合并:

将span的pageid改为previd,然后将管理页数增加合并的span的管理页数

将prevspan从spanlist中删除,最终删除管理前置页的对象

后向合并:

不合并条件和前向合并类似

开始后向合并:

直接改变管理页数即可,因为后向合并span的首pageid不变,其他操作于前向合并一样

在前后向合并完成后,我们直接挂载span到pagecache的桶中,并记录pageid和span的映射

3.6PageCache层与堆交互

(7)当pagecache中也没有所需大小的span,向堆中申请128page大小的span,并挂载到128page的桶中,进行后续切割,若足够就切割当前已有的span并返回

cpp 复制代码
Span* PageCache::NewSpan(size_t k)
{
	assert(k > 0 && k <= NPAGES);
	if (k > NPAGES - 1)
	{
		void* ptr = SystemAlloc(k);
		/*Span* span = new Span;*/
		//利用spanpool脱离new
		Span* span = _spanpool.New();
		span->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
		span->_n = k;
		_idspanmap[span->_pageId] = span;
		return span;
	}
	//检查第k个桶是否为空
	if (!_spanlists[k].Empty())
	{
		Span* kspan = _spanlists[k].PopFront();
		//建立page和span的映射关系,方便centralcache回收小块内存时,查找对应span位置
		for (int i = 0; i < kspan->_n; i++)
		{
			_idspanmap[kspan->_pageId + i] = kspan; 
			//_idspanmap.set((kspan->_pageId + i), kspan);//基数树优化
		}
		return kspan;
	}
	//检查后续的桶是否有span
	for (int i = k + 1; i < NPAGES; i++)
	{
		if (!_spanlists[i].Empty())
		{
			Span* nspan = _spanlists[i].PopFront();
			//Span* kspan = new Span;//获取k个空间
			Span* kspan = _spanpool.New();
			//在nspan中切割k页
			kspan->_pageId = nspan->_pageId;
			kspan->_n = k;
			nspan->_pageId += k;
			nspan->_n -= k;
			//将多余的span挂起来
			_spanlists[nspan->_n].PushFront(nspan);
			//存储首尾页号和nspan的映射,方便pagecache合并相邻页号的span空间
			_idspanmap[nspan->_pageId] = nspan;
			_idspanmap[nspan->_pageId + nspan->_n - 1] = nspan;
			//建立page和span的映射关系,方便centralcache回收小块内存时,查找对应span位置
			for (int i = 0; i < kspan->_n; i++)
			{
				_idspanmap[kspan->_pageId + i] = kspan;
			}
			return kspan;
		}
	}
	//向系统申请128页的span
	/*Span* bigspan = new Span;*/
	Span* bigspan = _spanpool.New();
	void* ptr = SystemAlloc(NPAGES-1);
	bigspan->_pageId = (size_t)ptr >> PAGE_SHIFT;//根据地址求页号
	bigspan->_n = NPAGES - 1;
	_spanlists[bigspan->_n].PushFront(bigspan);
	return NewSpan(k);//重新调用自己的函数,利用前面的切割代码
}

这里先忽略第一个if语句,只考虑线程单次申请空间小于256kb情况

首先检查拥有k页大小span的第k个桶是否还有span

情况1:若有就在建立完该span中每个pageid和该span的映射之后返回

情况2:若第k个桶没有span,我们尝试遍历剩下的具有更多页span的桶,切割合适大小的span出来返回,切剩下的继续挂载到spanlists中

情况3:若以上都不满足,向堆中申请128页大小的span,然后挂载到spanlists,再次调用NewSpan复用前面切割代码

3.7当用户单次申请空间大小大于256kb时

用户直接通过pagecache层的接口,间接向堆申请空间,并且在使用完毕后,通过pagecache层的接口释放空间到堆中

申请:

上层:concurrent.cpp

cpp 复制代码
static void* ConcurrentAlloc(size_t size)
{
	if (size > MAX_BYTES)//超过256kb
	{
		size_t alignsize = SizeClass::RoundUp(size);
		size_t kpage = alignsize >> PAGE_SHIFT;

		PageCache::GetInstance()->_pagemtx.lock();
		Span* span = PageCache::GetInstance()->NewSpan(kpage);
		span->_objsize = size;
		PageCache::GetInstance()->_pagemtx.unlock();

		void* ptr = (void*)(span->_pageId << PAGE_SHIFT);
		return ptr;
	}
	else
	{
		if (pTLSThreadCache == nullptr)
		{
			static ObjectPool<ThreadCache> tcpool;
			pTLSThreadCache = tcpool.New();
		}
		return pTLSThreadCache->Allocate(size);
	}
}

计算出需要申请的页数后,调用NewSpan进行申请,申请完毕后将地址返回

底层:

(1)申请大于256kb但是小于128page

直接进入centralcache申请span的代码逻辑即可,直接复用

(2)申请大于128page

cpp 复制代码
if (k > NPAGES - 1)
{
	void* ptr = SystemAlloc(k);
	/*Span* span = new Span;*/
	//利用spanpool脱离new
	Span* span = _spanpool.New();
	span->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
	span->_n = k;
	_idspanmap[span->_pageId] = span;
	return span;
}

使用系统调用申请,并做好成员变量初始化以及映射关联

疑问:为什么这里的映射只需要建立span首页的?

因为这是线程申请大于内存池管理上限的情况,他会作为一个整体使用,不会被切分,只要知道首页是否被使用就知道整块span是否被使用

释放:

上层:concurrent.cpp

cpp 复制代码
static void ConcurrentFree(void* ptr)
{
	Span* span = PageCache::GetInstance()->MapObjectToSpan(ptr);
	size_t size = span->_objsize;
	if (size > MAX_BYTES)
	{

		PageCache::GetInstance()->_pagemtx.lock();
		PageCache::GetInstance()->ReleaseSpanToPageCache(span);
		PageCache::GetInstance()->_pagemtx.unlock();

	}
	else
	{
		assert(pTLSThreadCache);
		pTLSThreadCache->Deallocate(ptr, size);
	}
}

当线程释放大于256kb时,调用Pagecache的释放接口

底层:

(1)大于256kb,小于128page

直接进入centralcache释放span的代码逻辑即可,直接复用

(2)大于128page

cpp 复制代码
//大于128页的向堆申请的空间释放
if (span->_n > NPAGES - 1)
{
	void* ptr = (void*)(span->_pageId << PAGE_SHIFT);
	SystemFree(ptr);
	/*delete span;*/
	_spanpool.Delete(span);
	return;
}

释放完实际空间后,将管理空间的span对象也释放

3.8优化

释放对象时优化为不传对象大小

给span添加_objsize成员变量,由于一个span只可能按照一个size大小切割,所以可以在centralcache申请pagecache的span的时候根据给定的切割大小赋值给_objsize,从而避免释放内存的时候传size

相关推荐
不光头强2 小时前
object所有方法及知识点
java·开发语言·jvm
.小小陈.2 小时前
C++进阶7:深入理解哈希表,从原理到 C++ 实践
开发语言·c++·学习·哈希算法
码云数智-大飞2 小时前
排序算法的终极博弈:从复杂度推导到工程选型实战
开发语言
南 阳2 小时前
Python从入门到精通day48
开发语言·python
keep intensify2 小时前
康复训练 2
c++
晨晖22 小时前
java容器类的博客
java·开发语言
leo__5202 小时前
MHT多假设跟踪算法(Multiple Hypothesis Tracking)MATLAB实现
开发语言·算法·matlab
燃于AC之乐2 小时前
深入解剖STL RB-tree(红黑树):用图解带入相关复杂操作实现
开发语言·c++·stl·红黑树·大厂面试·图解·插入操作
ShineWinsu2 小时前
对于C++中unordered_set的详细介绍
数据结构·c++·算法·面试·stl·哈希表·unordered_set