LOADING

加载过慢请开启缓存 浏览器默认开启

高并发内存池


内存池

内存池是一种预分配内存并进行重复利用的技术,通过减少频繁的动态内存分配和释放操作,从而提高程序的运行效率,内存池通常预先分配一块大的内存区域,将其划分很多小块。需要时直接从这块区域中分配,而不是直接调用系统的动态分配函数,可以有效的减少系统调用带来的开销

整体框架

三层缓存构成

thread cache

线程缓存是每个线程独有的,每个线程独享一个cache,这也是这个内存池高效的地方,
thread cache是一个哈希桶拉链法+自由链表的结构

#pragma once

#include "Common.h"

class ThreadCache
{
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);
private:
	FreeList _freeLists[NFREELIST];
};

// TLS thread local storage
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;

因为trheadcache使用的是哈希桶结构
不分段的话若全是按8字节分,则会需要32768个桶,但但是每个桶都是16Byte则会差生较多的内存碎片。因此这里采用了不同段的内存使用不同的内存对齐规则,既控制了桶的数量不会太多,又整体将内存碎片浪费控制在10%左右

内存对齐的函数的接口

//管理对齐和映射等关系
class SizeClass
{
public:
	//获取向上对齐后的字节数
	static inline size_t RoundUp(size_t bytes);
	//获取对应哈希桶的下标
	static inline size_t Index(size_t bytes);
};

对齐映射规则

static inline size_t _RoundUp(size_t bytes, size_t alignNum)
	{
		return ((bytes + alignNum - 1) & ~(alignNum - 1));
	}
	//内联函数:调用频繁,因此写成内联函数
	//向上对齐
	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//size > 256 * 1024 byte 
		{
			return _RoundUp(size, 1<<PAGE_SHIFT);
		}
	}

	static inline size_t _Index(size_t bytes, size_t align_shift)
	{
		return ((bytes + (1 << align_shift) - 1) >> align_shift) - 1;
	}
	//计算映射的哪一个自由链表桶中
	static inline size_t Index(size_t bytes)
	{
		assert(bytes <= MAX_BYTES);
		static int group_array[4] = { 16, 56, 56, 56 };
		if (bytes <= 128)
		{
			return _Index(bytes, 3);
		}
		else if (bytes <= 1024)
		{
			return _Index(bytes - 128, 4) + group_array[0];
		}
		else if (bytes <= 8 * 1024)
		{
			return _Index(bytes - 1024, 7) + group_array[0] + group_array[1];
		}
		else if (bytes <= 64 * 1024)
		{
			return _Index(bytes - 8 * 1024, 10) + group_array[0] + group_array[1] + group_array[2];
		}
		else if (bytes <= 256 * 1024)
		{
			return _Index(bytes - 64 * 1024, 13) + group_array[0] + group_array[1] + group_array[2] + group_array[3];
		}
		else
		{
			assert(false);
			return -1;
		}
	}

central cache

中心缓存所有线程共享,当我们的thread cache没有内存块时会向central cache申请,central cahce也是一个哈希桶的结构,不过它挂载的是Span List链表结构,Span List中则有一个个相同大小的内存块按照哈希桶的映射后,通过双向链表的形式挂在相应的Span上,

Central Cache本质是由一个哈希映射的span对象自由双向链表构成
为了保证全局只有唯一的Central Cache,这个类因此可以被设计称单例模式(这里使用的是饿汉模式)
饿汉模式:构造函数私有,对象设为静态私有。拷贝构造和赋值重载设为delete(防拷贝)


#pragma once

#include "Common.h"

// 单例模式
class CentralCache
{
public:
	static CentralCache* GetInstance()
	{
		return &_sInst;
	}
	// 获取一个非空的span
	Span* GetOneSpan(SpanList& list, size_t byte_size);
	// 从中心缓存获取一定数量的对象给thread cache
	size_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size);
	// 将一定数量的对象释放到span跨度
	void ReleaseListToSpans(void* start, size_t byte_size);
private:
	SpanList _spanLists[NFREELIST];
private:
	CentralCache()
	{}
	CentralCache(const CentralCache&) = delete;
	static CentralCache _sInst;
};

span的结构

//管理多个连续页的大块内存跨度结构
struct Span
{
	PAGE_ID _pageId = 0;//大块内存起始页的页号
	size_t _n = 0;//页的数量

	Span* _next = nullptr;//双向链表的结构
	Span* _prev = nullptr;

	size_t _objSize = 0;//切好的小对象的大小
	size_t _usecount = 0;//切好的小块内存,被分配给thread cache的计数
	void* _freeList = nullptr;//切好的小块内存的自由链表

	bool _isUse = false;//是否在被使用
};

使用双向链表来管理,下面是双向链表的结构
spanList

//带头双向循环链表
class SpanList
{
public:
	SpanList()
	{
		_head = new Span;
		_head->_next = _head;
		_head->_prev = _head;
	}

	void Insert(Span* pos, Span* newSpan)
	{
		assert(pos);
		assert(newSpan);
		Span* prev = pos->_prev;
		prev->_next = newSpan;
		newSpan->_prev = prev;
		newSpan->_next = pos;
		pos->_prev = newSpan;
	}

	Span* Begin()
	{
		return _head->_next;
	}

	Span* End()
	{
		return _head;
	}

	bool Empty()
	{
		return _head->_next == _head;
	}
	void PushFront(Span* span)
	{
		Insert(Begin(), span);
	}

	Span* PopFront()
	{
		Span* front = _head->_next;
		Erase(front);
		return front;
	}
	void Erase(Span* pos)
	{
		assert(pos);
		assert(pos != _head);

		Span* prev = pos->_prev;
		Span* next = pos->_next;
		prev->_next = next;
		next->_prev = prev;
	}
public:
	std::mutex _mtx;//桶锁
private:
	Span* _head;
};

page cache

存储的内存以page为单位存储以及分配的,当central cache没有内存对象时,我们从page cache分配出一定数量的page,并切割成定长大小的小块内存,分配给central cache,当一个span的几个跨度页的对象都收回以后,page cache会回收central cache满足条件的span对象,合并相邻的页,组成更大的页,这样缓解内存碎片问题

page的任务是
检查central cache申请的相应位置是否合法,如果没有合适的span,需要从较大的span中分裂两个
如果找到链表的最后都没有合适的span,则在系统中使用VirtualAlloc申请一个较大的span。
pagecache中的spanlist与前两个采用不同的内存对齐原则,直接就是1page,2page,3page
框架

#pragma once

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

class PageCache
{
public:
	static PageCache* GetInstance()
	{
		return &_sInst;
	}

	// 获取从对象到span的映射
	Span* MapObjectToSpan(void* obj);

	// 释放空闲span回到Pagecache,并合并相邻的span
	void ReleaseSpanToPageCache(Span* span);

	// 获取一个K页的span
	Span* NewSpan(size_t k);

	std::mutex _pageMtx;
private:
	SpanList _spanLists[NPAGES];
	ObjectPool<Span> _spanPool;

	//std::unordered_map<PAGE_ID, Span*> _idSpanMap;
	//std::map<PAGE_ID, Span*> _idSpanMap;
	TCMalloc_PageMap1<32 - PAGE_SHIFT> _idSpanMap;

	PageCache()
	{}
	PageCache(const PageCache&) = delete;


	static PageCache _sInst;
};