并发内存池的设计

0、大量的malloc/free小内存所带来的弊端

1.弊端

  • malloc/free的底层是系统调用,频繁的系统调用所带来的用户内核态切换花费大量时间, 大大降低系统执行效率;
  • 频繁的申请小内存, 带来的大量内存碎片, 内存使用率低下且导致无法申请大块的内存;
  • 没有内存回收机制, 很容易造成内存泄漏;

2.内存池的作用

  • 内存池提前预先分配大块内存,统一释放,极大的减少了malloc 和 free 等函数的调用
  • 内存池每次请求分配大小适度的内存块,最大避免了碎片的产生;
  • 在生命周期结束后统一释放内存,极大的避免了内存泄露的发生;

3.内存碎片出现原因解释

  • 内部内存碎片:  因为所有的内存分配必须起始于可被 4、8 或 16 整除的地址。假设当某个客户请求一个 43 字节的内存块时,因为没有适合大小的内存,所以它可能会获得 44字节、48字节等稍大一点的字节,因此由所需大小四舍五入而产生的多余空间就叫内部碎片
  • 外部内存碎片:频繁的分配与回收物理页面会导致大量的、连续且小的页面块夹杂在已分配的页面中间,就会产生外部碎片。

解决「外部内存碎片」的问题就是内存交换。可以把程序占用的那 256MB 内存写到硬盘上,然后再从硬盘上读回来到内存里。

1、内存池的演变

1、 最简单的内存分配器

描述:做一个链表指向空闲内存,分配就是取出一块来,改写链表。释放就是放回到链表里面,并做好归并。
优点: 实现简单。
缺点: 分配时搜索合适的内存块效率低,释放回归内存后归并消耗大,实际中不实用。

2、定长内存分配器

描述:每个固定内存分配器里面有两个链表,OpenList 用于存储未分配的空闲对象,CloseList用于存储已分配的内存对象,分配时从 OpenList 中取出一个对象放到 CloseList 里并且返回给用户,释放就是从 CloseList 移回到 OpenList。
优点: 分配和释放的效率高。
缺点: 只能解决定长的内存需求,长期占着内存没有释放。

3、哈希映射的FreeList 池

描述:在定长分配器的基础上,按照不同对象大小(8,16,32,64,128,256,512,1k…64K),构造多个固定内存分配器,分配内存时根据要申请内存大小进行对齐,然后查表决定到底由哪个分配器负责。
优点:分配和释放的效率高,可以解决一定长度内的问题。
缺点:存在内碎片的问题,长期占着内存没有释放。多线程并发场景下,锁竞争激烈,效率降低。

2、并发内存池的设计

2.1 内存池结构

  • 第一层是Thread Cache,线程缓存是每个线程独有的,在这里设计的是用于小于64k的内存分配,线程在这里申请不需要加锁,每一个线程都有自己独立的cache,这也就是这个项目并发高效的地方。
  • 第二层是Central Cache,在这里是所有线程共享的,它起着承上启下的作用,Thread Cache是按需要从Central Cache中获取对象,它就要起着平衡多个线程按需调度的作用,既可以将内存对象分配给Thread Cache来的每个线程,又可以将线程归还回来的内存进行管理。Central Cache是存在竞争的,所以在这里取内存对象的时候是需要加锁的,但是锁的力度可以控制得很小。
  • 第三层是Page Cache,存储的是以页为单位存储及分配的,Central Cache没有内存对象(Span)时,从Page Cache分配出一定数量的Page ,并切割成定长大小的小块内存,分配给Central CachePage Cache会回收Central Cache满足条件的Span(使用计数为0)对象,并且合并相邻的页,组成更大的页,缓解内存碎片的问题。

2.2 Thread Cache层

申请内存:

  • 当在Thread Cache中申请内存块(size<=64k)时,首先计算size大小的内存块所对应的freelist链表在freelist数组中的位置,如果对应freelist链表中有内存块时,直接从freelist[i]Pop出内存块,时间复杂度是O(1),且没有锁竞争。
  • freelist[i]中没有内存块时,则批量从Central Cache中获取一定数量的块,插入到自由链表并返回一个size大小的内存块。

释放内存:

  • 当释放内存小于64k时将内存释放回Thread Cache,首先计算size大小的内存块所对应的freelist链表在freelist数组中的位置,将内存块Pushfreelist[i]
  • 当链表的长度过长,空闲的内存块数量过多,也就是超过freelist链表最大容纳内存块数目时,则将所有内存块回收到Central Cache
class ThreadCache
{
private:
	Freelist _freelist[NLISTS];//自由链表数组
public:
	//申请内存对象
	void* Allocate(size_t size);
	//释放内存对象
	void Deallocate(void* ptr, size_t size);
	//从中心缓存获取对象
	void* FetchFromCentralCache(size_t index, size_t size);
	//释放内存对象时,对应索引的自由链表过长时,回收内存到中心缓存
	void ListTooLong(Freelist* list, size_t size);
};
//**静态的,不是所有可见 !!!!**
//每个线程有个自己的指针, 用(_declspec (thread)),我们在使用时,每次来都是自己的,就不用加锁了
//每个线程都有自己的tlslist
_declspec (thread) static ThreadCache* tlslist = nullptr;

/*
 * 从中心缓存获取对象,每一次取批量的数据。
 * 因为每次到CentralCache申请内存的时候是需要加锁的,所以一次就多申请一些内存块,
 * 防止每次到CentralCache去内存块的时候,多次加锁造成效率问题
 */
void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{
	//获得指向该索引对应自由链表对象的指针
	Freelist* freelist = &_freelist[index];
	/* 不是每次申请10个,而是进行慢增长的过程,申请内存块数量原则如下:
	(1)单个对象越小,申请内存块的数量越多
	(2)单个对象越大,申请内存块的数量越少
	(3)申请次数越多,申请内存块的数量越多
	(4)申请次数越少,申请内存块的数量越少*/
	size_t maxsize = freelist->MaxSize();
	//获得需要从中心缓冲中拉取内存块的数量
	size_t numtomove = min(SizeClass::NumMoveSize(size), maxsize);
	//start, end分别表示取出来的内存的开始地址和结束地址
	//取出来的内存是一个链在一起的内存对象,需要首尾标识
	void* start = nullptr;
	void* end = nullptr;
	//batchsize表示实际取出来的size大小缓存的个数
	//batchsize可能小于numtomove,因为可能对应的中心缓存中span对象的list内没有那么多大小的缓存块
	size_t batchsize = CentralCache::Getinstence()->FetchRangeObj(start, end, numtomove, size);
	//当申请的缓存个数大于1,则舍弃申请的缓存链表的第一个内存节点【为什么!!!】
	if (batchsize > 1)
	{
		freelist->PushRange(NEXT_OBJ(start), end, batchsize - 1);
	}
	//当threadCache层申请的size大小缓存达到链表容纳节点最大值时,将链表容量扩增1
	if (batchsize >= freelist->MaxSize())
	{
		freelist->SetMaxSize(maxsize + 1);
	}
	//返回申请的多个内存组成链表的首地址
	return start;
}

/*
 * 释放内存对象时,对应索引的自由链表过长时,回收内存到中心缓存
 */
void ThreadCache::ListTooLong(Freelist* freelist, size_t size)
{
	//将freelist自由链表中内存对象个数置0
	//返回指向自由链表首地址的指针
	void* start = freelist->PopRange();
	//将其释放回中心缓存的对应spanlist的span中
	CentralCache::Getinstence()->ReleaseListToSpans(start, size);
}

/*
 * 申请内存对象,从数组中对应索引的自由链表中弹出一个对象,作为申请的内存对象使用
 */
void* ThreadCache::Allocate(size_t size)
{
	//计算size大小的对象应该对应自由链表数组中哪个索引
	size_t index = SizeClass::Index(size);
	//获得指向该索引对应自由链表对象的指针
	Freelist* freelist = &_freelist[index];
	//判断链表是否为空,如果链表不为空,则从中弹出一个对象指针
	if (!freelist->Empty())
	{
		return freelist->Pop();
	}
	//如果自由链表为空,就要去中心缓存中拿取内存对象,一次拿取多个,防止多次去取而加锁带来的开销
	/*【均衡策略】:每次中心堆分配给ThreadCache对象的个数是个慢启动策略
		随着取的次数增加而内存对象个数增加,防止一次给其他线程分配太多,而另一些线程申请
		内存对象的时候必须去PageCache去取,带来效率问题。*/
	else
	{
		//从中心缓存CenterCache中获取内存
		return FetchFromCentralCache(index, SizeClass::Roundup(size));
	}
}

/*
 * 释放内存对象,将一个大小为size的对象压入对应索引的自由链表中
 */
void ThreadCache::Deallocate(void* ptr, size_t size)
{
	//计算size大小的对象应该对应自由链表数组中哪个索引
	size_t index = SizeClass::Index(size);
	//获得指向该索引对应自由链表对象的指针
	Freelist* freelist = &_freelist[index];
	//将对象压入对应的自由链表中(头插法)
	freelist->Push(ptr);
	//满足链表内当前对象个数大于设定链表内对象个数上限时
	//释放一个批量的对象,释放回中心缓存CenterCache
	if (freelist->Size() >= freelist->MaxSize())
	{
		ListTooLong(freelist, size);
	}
}

2.3 Central Cache层

申请内存:

  • Thread Cache中没有内存时,就会批量向Central Cache申请一些内存对象,Central Cache有一个哈希映射的spanlistspanlist中挂着span,从span中的list取出内存块给Thread Cache,这个过程是需要加锁的。
  • Central Cache中没有非空的span时,则将空的span链在一起,向Page Cache申请一个newspan对象,将newspan对象中以页为单位的内存,切成需要的内存块大小,并链接起来,挂到newspan中。再将该newspan插入到Central Cache对应的spanlist链表中。
  • Central Cachespan中有一个_usecount,每从span中分配一个内存块给Thread Cache,就++_usecount

释放内存:

  • Thread Cache过长或者线程销毁,则会将内存块释放回Central Cache中的,每个内存块被释放时,其所属span对象就会执行- -_usecount
  • _usecount减到0时则表示所有内存块都回到了span,则将span释放回Page Cache,如果span对象的内存块总量超过128页,则直接向系统释放该span对象,否则Page Cache中会对该span对象前后相邻的空闲页进行合并。

特别关心:怎么才能将Thread Cache中的内存对象还给它原来的span呢?
答:可以在Page Cache中维护一个pair<pageid,  span对象>的映射,当Span Cache给Central Cache分配一个span时,将这个映射更新到unordered_map中去,在Thread Cache还给Central Cache时,可以查这个unordered_map找到对应的span。

pageid = 地址void* pir/(1<<12),其中(1<<12)为1页所占字节数。

/*
 * 进行资源的均衡,对于ThreadCache的某个资源过剩时,可以回收ThreadCache内部的内存
 * 从而可以分配给其它的ThreadCache;当threadCache对应size的自由链表中没有空闲内存块,要从中心缓存获取
 *
 * 只有一个中心缓存,对于所有的线程来获取内存的时候都应该是一个中心缓存,
 * 所以对于中心缓存可以使用单例模式来进行创建中心缓存的类,
 * 对于中心缓存来说要加锁。
 */

 //设计成单例模式
class CentralCache
{
public:
	static CentralCache* Getinstence()
	{
		return &_inst;
	}
	//从page cache获取一个span
	Span* GetOneSpan(SpanList& spanlist, size_t byte_size);
	//从中心缓存中获取一定数量的对象给threadCache
	size_t FetchRangeObj(void*& start, void*& end, size_t n, size_t byte_size);
	//将一定数量的对象释放给span
	void ReleaseListToSpans(void* start, size_t size);
private:
	SpanList _spanlist[NLISTS];
private:
	//声明构造函数,防止默认构造
	CentralCache() {}
	//强制默认不生成复制构造函数,防止浅拷贝
	CentralCache(CentralCache&) = delete;
	static CentralCache _inst;
};

CentralCache CentralCache::_inst;
/*
 *从CenterCache获取一个span
 */
Span* CentralCache::GetOneSpan(SpanList& spanlist, size_t byte_size)
{
	//遍历当前span链表,找出空闲的span对象,找到则直接返回该空闲对象节点
	Span* span = spanlist.Begin();
	while (span != spanlist.End())
	{
		//span链表节点中的list成员,标志该节点是否空闲,list!=nullptr,说明该span节点还有内存可分配
		if (span->_list != nullptr)
			return span;
		else
			span = span->_next;
	}
	//走到这,说明当前中心缓存的span链表中没有获取到空闲的span节点对象,到pageCache层获取newspan
	Span* newspan = pageCache::GetInstence()->NewSpan(SizeClass::NumMovePage(byte_size));
	/*将newspan对象切分成需要的内存块并链接起来*/
	//通过对newspan节点的pageid计算获得newspan对象的起始地址
	char* cur = (char*)(newspan->_pageid << PAGE_SHIFT);
	//通过newspan的起始地址+newspan对象的大小,获得newspan对象的终止地址
	char* end = cur + (newspan->_npage << PAGE_SHIFT);
	//将新分配的span对象[cur, end]分割成由一个个byte_size大小的内存块串联起来的链表,存放在newspan对象的list成员中
	newspan->_list = cur;//span中可分配内存块链表首地址
	newspan->_objsize = byte_size;//span中可分配内存块的单位大小
	//串联链表
	while (cur + byte_size < end)
	{
		char* next = cur + byte_size;
		NEXT_OBJ(cur) = next;
		cur = next;
	}
	NEXT_OBJ(cur) = nullptr;
	//往中心缓存对应索引的spanlist链表中插入新span对象
	spanlist.PushFront(newspan);
	return newspan;
}

/*
 *从中心缓存中获取一定数量的内存块给threadCache
 */
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t n, size_t byte_size)
{
	//获取size大小的内存块对应的spanlist链索引
	size_t index = SizeClass::Index(byte_size);
	SpanList& spanlist = _spanlist[index];//赋值->拷贝构造
	//记得加锁
	//spanlist.Lock();
	std::unique_lock<std::mutex> lock(spanlist._mutex);
	//获取一个span用于分割出多个byte_size大小的内存块
	Span* span = GetOneSpan(spanlist, byte_size);
	//从span对象的list链表中获取range[start, end]的n个内存块
	size_t batchsize = 0;
	void* prev = nullptr;//提前保存前一个
	void* cur = span->_list;//用cur来遍历,往后走
	for (size_t i = 0; i < n; ++i)
	{
		prev = cur;
		cur = NEXT_OBJ(cur);
		++batchsize;//累积byte_size大小缓存的个数
		//随时判断cur是否为空,为空的话,提前停止
		if (cur == nullptr)
			break;
	}
	//赋值被分配缓存的首地址和尾地址
	start = span->_list;
	end = prev;
	//从中心缓存的span节点中,获取[start, end]区间内的多个byte_size大小缓存
	//此时移动span节点中的list链表指针,如果到end->next处,如果end==nullptr说明当前span节点已全部用完
	span->_list = cur;
	//累计当前span对象被使用的byte_size大小内存块的数量
	span->_usecount += batchsize;
	//如果当前span对象节点已经被分配完,则将空的span移到spanlist链表最后,保持非空的span在前面
	if (span->_list == nullptr)
	{
		spanlist.Erase(span);
		spanlist.PushBack(span);
	}
	//spanlist.Unlock();
	//返回分配的byte_size大小缓存数量
	return batchsize;
}

/*
 *将一定数量的内存块释放给span
 */
void CentralCache::ReleaseListToSpans(void* start, size_t size)
{
	//计算分配该内存块的span所在的spanlist链表位置
	size_t index = SizeClass::Index(size);
	SpanList& spanlist = _spanlist[index];
	//将锁放在循环外面
	// CentralCache:对当前桶进行加锁(桶锁),减小锁的粒度
	// PageCache:必须对整个SpanList全局加锁
	// 因为可能存在多个线程同时去系统申请内存的情况
	//spanlist.Lock();
	std::unique_lock<std::mutex> lock(spanlist._mutex);
	//采用头插法,逐个内存块地释放到size大小内存块对应的span对象的list链表中
	while (start)
	{
		void* next = NEXT_OBJ(start);
		//记得加锁
		//spanlist.Lock(); // 构成了很多的锁竞争
		//获取内存块start地址对应的页号,根据对应页号再map中的映射关系找到该内存块所属span对象
		//因为链表串联的不同内存块,在虚拟地址上可能不属于同一页
		Span* span = PageCache::GetInstence()->MapObjectToSpan(start);
		//头插法,将当前被释放节点的尾部指向原span对象list链表的第一个内存块
		NEXT_OBJ(start) = span->_list;
		//更新span对象得第一个内存块指针
		span->_list = start;
		//当一个span的对象全部释放回来的时候,将span还给pagecache,并且做页合并
		if (--span->_usecount == 0)
		{
			//将span节点从当前spanlist链表中移除(并未释放被移除的span对象)
			spanlist.Erase(span);
			PageCache::GetInstence()->ReleaseSpanToPageCache(span);
		}
		//spanlist.Unlock();
		start = next;
	}
	//spanlist.Unlock();
}

2.4 Page Cache层

申请内存:

  • Central CachePage cache申请内存时,Page cache先检查对应spanlist链表有没有span,如果没有则向更大页寻找一个span,如果找到则分裂成两个。
  • 比如:申请的是4page4page后面没有挂span,则向后面寻找更大的span,假设在10page spanlist位置找到一个span,则将10page span分裂为一个4page span和一个6page span。将4page span返回给Central Cache,并将6page span所对应的span插入对应的spanlist中。
  • 如果找到128 page spanlist都没有合适的span,则向系统使用mmapbrk或者是VirtualAlloc等方式申请128 page span挂在自由链表中,再重复1中的过程。

释放内存:

  • 如果Central Cache释放回一个span,如果该span内存块总量大于128页,则直接将该span释放回系统中。
  • 否则依次寻找span的前后_pageidspan,看是否可以合并,如果合并继续向前寻找。这样就可以将切小的内存合并收缩成大的span,减少内存碎片。当合并span内存块总量大于128页,将该span释放回系统中。
//对于Page Cache也要设置为单例,对于Central Cache获取span的时候
//每次都是从同一个page数组中获取span
//单例模式
class PageCache
{
public:
	static PageCache* GetInstence()
	{
		return &_inst;
	}
	Span* AllocBigPageObj(size_t size);
	void FreeBigPageObj(void* ptr, Span* span);
	Span* _NewSpan(size_t n);
	Span* NewSpan(size_t n);//获取的是以页为单位
	//获取从对象到span的映射
	Span* MapObjectToSpan(void* obj);
	//释放空间span回到PageCache,并合并相邻的span
	void ReleaseSpanToPageCache(Span* span);
private:
	//spanlist数组
	SpanList _spanlist[NPAGES];
	//std::map<PageID, Span*> _idspanmap;
	//map中存放<pageid, span>的映射关系,其中pageid可以由地址ptr计算得来
	//因此可以认为map中存放“地址ptr与span对象的映射”
	std::unordered_map<PageID, Span*> _idspanmap;
	//页缓存锁
	std::mutex _mutex;
private:
	//声明构造函数,防止默认构造
	PageCache() {}
	//强制默认不生成复制构造函数,防止浅拷贝
	PageCache(const PageCache&) = delete;
	static PageCache _inst;
};

PageCache PageCache::_inst;

/*
 *大对象申请,直接从系统
 */
Span* PageCache::AllocBigPageObj(size_t size)
{
	//需要申请的内存大小大于64k
	assert(size > MAX_BYTES);
	//将需要分配的内存大小进行内存对齐,即转换成大于等于size的内存最小单位倍数
	size = SizeClass::_Roundup(size, PAGE_SHIFT);
	//将内存大小转换成页数量
	size_t npage = size >> PAGE_SHIFT;
	//情况一:需要申请的内存小于128k
	if (npage < NPAGES)
	{
		//尝试在pageCache层的Spanlist中找到空闲链表中的Span返回
		Span* span = NewSpan(npage);
		span->_objsize = size;
		return span;
	}
	//情况二:当申请的内存大于128k,则直接向系统申请内存
	else
	{
		void* ptr = VirtualAlloc(0, npage << PAGE_SHIFT,MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
		if (ptr == nullptr)
			throw std::bad_alloc();
		//构造一个Span对象描述从系统申请来的内存
		Span* span = new Span;
		span->_npage = npage;//Span对象的可分配内存页数
		span->_pageid = (PageID)ptr >> PAGE_SHIFT; //Span对象的pageid
		span->_objsize = npage << PAGE_SHIFT;//Span对象的可分配内存大小
		//构建该span对象与pageid的映射关系
		_idspanmap[span->_pageid] = span;
		//返回申请的内存所构成的span对象
		return span;
	}
}

/*
 *释放大对象,直接从系统
 */
void PageCache::FreeBigPageObj(void* ptr, Span* span)
{
	//计算释放的Span对象的内存大小
	size_t npage = span->_objsize >> PAGE_SHIFT;
	//情况二:当释放的内存小于128页,直接从内存释放,即在pageCache中合并
	if (npage < NPAGES)
	{
		span->_objsize = 0;
		ReleaseSpanToPageCache(span);
	}
	//情况二:当释放的内存大于128页,直接从内存释放
	else
	{
		_idspanmap.erase(npage);
		delete span;
		VirtualFree(ptr, 0, MEM_RELEASE);
	}
}

/*
 *新申请一个n页大小的Span
 */
Span* PageCache::NewSpan(size_t n)
{
	// 加锁,防止多个线程同时到PageCache中申请span
	// 这里必须是给全局加锁,不能单独的给每个桶加锁
	// 如果对应桶没有span,是需要向系统申请的
	// 可能存在多个线程同时向系统申请内存的可能
	std::unique_lock<std::mutex> lock(_mutex);
	return _NewSpan(n);
}

/*
 *新申请一个n页大小的Span
 */
Span* PageCache::_NewSpan(size_t n)
{
	//第一步首先确定需要从pageCache获取的页数小于等于128页
	assert(n < NPAGES);
	//判断索引n对应的span链表是否为空,如果不为空则弹出一个span节点返回给中心缓存
	if (!_spanlist[n].Empty())
		return _spanlist[n].PopFront();
	//走到这说明索引为n的span链表为空,此时需要往n+1方向获取空闲span节点
	for (size_t i = n + 1; i < NPAGES; ++i)
	{
		if (!_spanlist[i].Empty())
		{
			//找到第一个空闲span节点,将其从spanlist链表中弹出
			Span* span = _spanlist[i].PopFront();
			//创建新的span对象,承接所需的n*page大小的span对象
			Span* splist = new Span;
			//新span对象的pageid为源span对象pageid
			splist->_pageid = span->_pageid;
			//初始化新span对象的页数npage
			splist->_npage = n;
			//源span对象的pageid增加n
			span->_pageid = span->_pageid + n;
			//源span对象的页数减n
			span->_npage = span->_npage - n;
			//在pageid与span的映射map上,对应[span->pageid, span->pageid+n]位置映射对象为splist
			for (size_t i = 0; i < n; ++i)
				_idspanmap[splist->_pageid + i] = splist;
			//将源span对象分割n*page后的span对象压入到页缓存中对应页数的spanlist链表中
			_spanlist[span->_npage].PushFront(span);
			//返回新span对象
			return splist;
		}
	}
	Span* span = new Span;

	// 到这里说明SpanList中没有合适的span,只能向系统申请128页的内存
#ifdef _WIN32
	void* ptr = VirtualAlloc(0, (NPAGES - 1) * (1 << PAGE_SHIFT), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
	//  brk
#endif
	//初始化span对象的pageid = 对象首地址整数化/每页的字节数
	//因为两个span对象的整数化地址差即是相差对应字节数,字节数/页字节数 == 页数
	span->_pageid = (PageID)ptr >> PAGE_SHIFT;
	//设置新span对象的页数为128
	span->_npage = NPAGES - 1;
	//在pageid与span的映射map上,对应[span->pageid, span->pageid+128]位置映射对象为新对象span
	for (size_t i = 0; i < span->_npage; ++i)
		_idspanmap[span->_pageid + i] = span;
	//将新对象压入到索引为128的spanlist链表中
	_spanlist[span->_npage].PushFront(span);  //方括号
	return _NewSpan(n);
}

/*
 *获取从对象到span的映射
 */
Span* PageCache::MapObjectToSpan(void* obj)
{
	//计算页号
	PageID id = (PageID)obj >> PAGE_SHIFT;
	//根据页号pageid获得对象obj的映射span对象
	auto it = _idspanmap.find(id);
	if (it != _idspanmap.end())
	{
		//返回该pageid对应的span对象
		return it->second;
	}
	else
	{
		assert(false);
		return nullptr;
	}
}

/*
 *将一个Span从PageCache释放
 *情况一:当释放的内存是大于128页,直接将内存归还给操作系统,不能合并
 *情况二:当释放的内存是小于128页,将当前Span与前后空闲Span合并
 */
void PageCache::ReleaseSpanToPageCache(Span* cur)
{
	// 必须上全局锁,可能多个线程一起从ThreadCache中归还数据
	std::unique_lock<std::mutex> lock(_mutex);
	// 当释放的内存是大于128页,直接将内存归还给操作系统,不能合并
	if (cur->_npage >= NPAGES)
	{
		//将span根据spanid计算处实际地址ptr
		void* ptr = (void*)(cur->_pageid << PAGE_SHIFT);
		// 归还之前,先删除掉页到span的映射
		_idspanmap.erase(cur->_pageid);
		VirtualFree(ptr, 0, MEM_RELEASE);
		delete cur;
		return;
	}
	// 向前合并
	while (1)
	{
		//超过128页则不合并
		PageID curid = cur->_pageid;
		PageID previd = curid - 1;
		auto it = _idspanmap.find(previd);
		// 没有找到
		if (it == _idspanmap.end())
			break;
		// 前一个span不空闲,即还有内存空间再threadCache中未被释放的
		if (it->second->_usecount != 0)
			break;
		Span* prev = it->second;
		//超过128页则不合并
		if (cur->_npage + prev->_npage > NPAGES - 1)
			break;
		// 先把prev从spanlist链表中移除
		_spanlist[prev->_npage].Erase(prev);
		// cur与prev合并
		prev->_npage += cur->_npage;
		//修正id->span的映射关系
		for (PageID i = 0; i < cur->_npage; ++i)
		{
			_idspanmap[cur->_pageid + i] = prev;
		}
		delete cur;

		// 继续向前合并
		cur = prev;
	}
	//向后合并
	while (1)
	{
		//超过128页则不合并
		PageID curid = cur->_pageid;
		PageID nextid = curid + cur->_npage;
		auto it = _idspanmap.find(nextid);
		if (it == _idspanmap.end())
			break;
		if (it->second->_usecount != 0)
			break;
		Span* next = it->second;
		//超过128页则不合并
		if (cur->_npage + next->_npage >= NPAGES - 1)
			break;
		_spanlist[next->_npage].Erase(next);
		cur->_npage += next->_npage;
		//修正id->Span的映射关系
		for (PageID i = 0; i < next->_npage; ++i)
		{
			_idspanmap[next->_pageid + i] = cur;
		}
		delete next;
	}
	// 最后将合并好的span插入到当前的spanlist链中
	_spanlist[cur->_npage].PushFront(cur);
}

2.5 向系统申请内存

3. 项目不足及扩展学习

  • 项目的独立性不足:
  1. 不足:当前实现的项目中我们并没有完全脱离malloc,比如在内存池自身数据结构的管理中,如SpanList中的span等结构,我们还是使用的new Span这样的操作,new的底层使用的是malloc,所以还不足以替换malloc,因为们本身没有完全脱离它。
  2. 解决方案:项目中增加一个定长的ObjectPool的对象池,对象池的内存直接使用brk、VirarulAlloc等向系统申请,new Span替换成对象池申请内存。这样就完全脱离的malloc,就可以替换掉malloc。
  • 平台及兼容性:
  1. linux等系统下面,需要将VirtualAlloc替换为brk等。
  2. x64系统下面,当前的实现支持不足。比如:id查找Span得到的映射,我们当前使用的是map<pageid,Span*>。在64位系统下面,这个数据结构在性能和内存等方面都是撑不住。需要改进后基数树。
    具体参考:
    基数树(radix tree)_uestc-leon的博客-CSDN博客_基数树

附录源码,包含构造freelist对象、span对象、spanlist对象,以及内存对齐等相关代码:

const size_t MAX_BYTES = 64 * 1024; //ThreadCache可申请的最大内存
const size_t NLISTS = 184; //每个线程的threadCache数组元素总的有多少个,由对齐规则计算得来
const size_t PAGE_SHIFT = 12; //用于在字节于页之间的单位转换
const size_t NPAGES = 129; //是否直接从系统中申请/释放的页数上限

/*
 * 获取指向obj对象下一对象的指针的引用
 */
inline static void*& NEXT_OBJ(void* obj)
{
	//先强转为void**,然后解引用就是一个void*
	//(1)先强转void**,此时obj为指向指针的指针
	//(2)解引用后获得对象内部的指向的指针,即指向obj下一对象的指针
	return *((void**)obj);
}

/*
 * ThreadCache层使用
 * 公共的FreeList对象,每个对象还有多个接口,用一个类来管理自由链表
 */
class Freelist
{
private:
	void* _list = nullptr; //始终指向自由链表的第一个对象
	size_t _size = 0;	//自由链表当前大小
	size_t _maxsize = 1;//自由链表最大允许的大小(链表内对象个数超过该值,则将去合并释放到中心缓存CenterCache中)
public:
	//将对象头插法插入链表头部,更新链表内对象个数
	void Push(void* obj)
	{
		NEXT_OBJ(obj) = _list;
		_list = obj;
		++_size;
	}
	//采用头插法像链表list中插入[start,end]范围内串联的节点链表
	void PushRange(void* start, void* end, size_t n)
	{
		NEXT_OBJ(end) = _list;
		_list = start;
		_size += n;
	}
	//将对象从链表头部弹出,并返回被弹出的对象指针
	void* Pop() //把对象弹出去
	{
		void* obj = _list;
		_list = NEXT_OBJ(obj);
		--_size;
		return obj;
	}
	//将链表内的对象个数置零,返回链表首地址副本
	void* PopRange()
	{
		_size = 0;
		void* list = _list;
		_list = nullptr;
		return list;
	}
	//list指针永远指向自由链表的头节点,大概list指针指向nullptr,说明改链表为空
	bool Empty()
	{
		return _list == nullptr;
	}
	//返回当前链表中对象的数量
	size_t Size()
	{
		return _size;
	}
	//返回自由链表的容量大小
	size_t MaxSize()
	{
		return _maxsize;
	}
	//设置自由链表的容量
	void SetMaxSize(size_t maxsize)
	{
		_maxsize = maxsize;
	}
};

/*
 *专门用来计算大小位置的类
 */
class SizeClass
{
public:
	//获取size大小的对象在FreeList数组中的位置,其中align表示要对齐应该移动的位数
	inline static size_t _Index(size_t size, size_t align)
	{
		size_t alignnum = 1 << align;  //库里实现的方法
		return ((size + alignnum - 1) >> align) - 1;
	}
	//用于内存对齐,找出第一个大于等于size的内存单位大小的倍数
	inline static size_t _Roundup(size_t size, size_t align)
	{
		size_t alignnum = 1 << align;
		return (size + alignnum - 1) & ~(alignnum - 1);
	}
public:
	// 控制在12%左右的内碎片浪费
	// [1,128]				8byte对齐 freelist[0,16)
	// [129,1024]			16byte对齐 freelist[16,72)
	// [1025,8*1024]		128byte对齐 freelist[72,128)
	// [8*1024+1,64*1024]	1024byte对齐 freelist[128,184)
	//获取size大小的内存对象所对应的freelist链表在数组中的索引
	inline static 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 <= 8192)
		{
			return _Index(size - 1024, 7) + group_array[0] + group_array[1];
		}
		else//if (size <= 65536)
		{
			return _Index(size - 8 * 1024, 10) + group_array[0] + group_array[1] + group_array[2];
		}
	}
	// 对齐大小计算,向上取整
	static inline size_t Roundup(size_t bytes)
	{
		assert(bytes <= MAX_BYTES);
		if (bytes <= 128) {
			return _Roundup(bytes, 3);
		}
		else if (bytes <= 1024) {
			return _Roundup(bytes, 4);
		}
		else if (bytes <= 8192) {
			return _Roundup(bytes, 7);
		}
		else {//if (bytes <= 65536){
			return _Roundup(bytes, 10);
		}
	}
	//动态计算从中心缓存分配多少个内存对象到ThreadCache中
	static size_t NumMoveSize(size_t size)
	{
		if (size == 0)
			return 0;
		//MAX_BYTES为64k,需要分配的对象size越大,分配的内存对象越少
		//上限为512, 下限为2
		int num = (int)(MAX_BYTES / size);
		if (num < 2)
			num = 2;
		if (num > 512)
			num = 512;
		return num;
	}
	// 根据size计算中心缓存要从页缓存获取多大的span对象
	static size_t NumMovePage(size_t size)
	{
		//第一步先计算需要分配的size对象数量
		size_t num = NumMoveSize(size);
		//计算分配num个size所需的页数
		size_t npage = num * size;
		npage >>= PAGE_SHIFT;
		//确保至少分配1页
		if (npage == 0)
			npage = 1;
		return npage;
	}
};

#ifdef _WIN32
typedef size_t PageID;
#else
typedef long long PageID;
#endif //_WIN32
//页节点结构
struct Span
{
	PageID _pageid = 0;//页号
	size_t _npage = 0;//当前Span节点的页数
	Span* _prev = nullptr; //指向当前页节点的上一节点的指针
	Span* _next = nullptr;//指向当前页节点的下一节点的指针
	void* _list = nullptr;//链接对象的自由链表,后面有对象就不为空,没有对象就是空
	size_t _objsize = 0;//对象的大小
	//页节点对象引用计数,分配一个对象给Thread Cache,就++usecount。
	//当ThreadCache过长或者线程销毁,则会将内存释放回Central Cache中的,释放回来时--usecount。
	//当usecount减到0时则表示所有对象都回到了span,则将Span释放回Page Cache
	size_t _usecount = 0;//对象使用计数,
};

/*
 * CenterCache层使用
 * 公共的SpanList对象,每个对象还有多个接口,用一个类来管理双向带头循环链表
 */
class SpanList
{
public:
	Span* _head;//双向循环链表的头节点,是个空节点,不分配内存大小
	std::mutex _mutex;//中心内存使用互斥量
public:
	/*构造函数,替代默认构造函数*/
	SpanList()
	{
		_head = new Span;
		_head->_next = _head;
		_head->_prev = _head;
	}
	/*析构函数,释放链表的每个节点*/
	~SpanList()
	{
		//head是个空节点,所以不用释放,直接从head->next开始释放
		Span* cur = _head->_next;
		while (cur != _head)
		{
			Span* next = cur->_next;
			delete cur;
			cur = next;
		}
		delete _head;
		_head = nullptr;
	}
	//防止拷贝构造和赋值构造,将其封死,没有拷贝的必要,不然就自己会实现浅拷贝
	//【由于对象内部涉及指针的成员变量,浅拷贝会造成一个指针被多次重复释放!!!】
	SpanList(const SpanList&) = delete;
	SpanList& operator=(const SpanList&) = delete;
	/*左闭右开——返回第一个数据的指针*/
	Span* Begin()
	{
		//因为head是一个空的头节点
		return _head->_next;
	}
	/*左闭右开——返回最后一个的下一个指针*/
	Span* End()
	{
		//因为head是一个空的头节点
		return _head;
	}
	/*判断链表是否为空*/
	bool Empty()
	{
		//当链表中第一个数据指针是空头节点,则说明该链表为空
		return _head->_next == _head;
	}
	/*在cur位置的前面插入一个页节点newspan*/
	void Insert(Span* cur, Span* newspan)
	{
		//获取当前节点的上一个节点指针
		Span* prev = cur->_prev;
		//按顺序排列pre newspan cur
		prev->_next = newspan;
		newspan->_next = cur;
		newspan->_prev = prev;
		cur->_prev = newspan;
	}
	/*假删除pos位置的节点,此处只是单纯的把pos拿出来,并没有释放掉,后面还有用处*/
	void Erase(Span* cur)
	{
		Span* prev = cur->_prev;
		Span* next = cur->_next;
		prev->_next = next;
		next->_prev = prev;
	}
	/*尾插*/
	void PushBack(Span* newspan)
	{
		Insert(End(), newspan);
	}
	/*头插*/
	void PushFront(Span* newspan)
	{
		Insert(Begin(), newspan);
	}
	/*尾删,实际是将尾部位置的节点拿出来*/
	Span* PopBack()
	{
		Span* span = _head->_prev;
		Erase(span);
		return span;
	}
	/*头删,实际是将头部位置节点拿出来*/
	Span* PopFront()
	{
		Span* span = _head->_next;
		Erase(span);
		return span;
	}
	/*给中心缓存CenterCache层上锁*/
	void Lock()
	{
		_mutex.lock();
	}
	/*给中心缓存CenterCache层解锁*/
	void Unlock()
	{
		_mutex.unlock();
	}
};

参考:详谈内存池原理及实现 - 知乎

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值