四、高并发内存池整体框架设计

四、高并发内存池整体框架设计

现代很多的开发环境都是多核多线程,在申请内存的场景下,必然存在激烈的锁竞争问题。malloc本身其实已经很优秀,那么我们项目的原型TCmalloc就是在多线程高并发的场景下更胜一筹,所以这次我们实现的内存池需要考虑以下几方面的问题。

  1. 性能问题。
  2. 多线程环境下,锁竞争问题。
  3. 内存碎片问题。

concurrent memory pool主要由以下3个部分构成:

1、thread cache:线程缓存是每个线程独有的,用于分配小于256KB的内存的,线程从这里申请内存不需要加锁,每个线程独享一个thread cache,这也就是这个并发线程池高效的地方。

2、central cache:中心缓存是所有线程所共享,thread cache是按需从central cache中获取的对象。central cache合适的时机回收thread cache中的对象,避免一个线程占用了太多的内存,而其他线程的内存不够用,达到内存分配在多个线程中更均衡的按需调度的目的。central cache是存在竞争的,所以从这里取内存对象是需要加锁,首先这里用的是桶锁,其次只有thread cache的没有内存对象时才会找central cache,所以这里竞争也就不会很激烈。

3、page cache:页缓存是在central cache缓存上面的一层缓存,存储的内存是以页为单位存储及分配的,central cache没有内存对象时,从page cache分配出一定数量的page,并切割成定长大小的小块内存,分配给central cache。当一个span的几个跨度页的对象都回收以后,page cache会回收central cache满足条件的span对象,并且合并相邻的页,组成更大的页,缓解内存碎片的问题,当central cache再一次向PageCache申请更大块内存的时候PageCache也能满足。

内存池三层模型中函数需要用到的公共的头文件--common.h

cpp 复制代码
//能够申请的内存的最大值
static const size_t MAX_BYTES = 256 * 1024;
//thread_cache的哈希桶的数目
static const size_t NFREELIST = 208;
//PageCache中哈希桶的数量,每一个桶按照页数映射
//每一个页数对应的桶的span对象的大小就是页数,即第一页的span对象的大小是一页
static const size_t NPAGES = 129;
//页大小转换偏移, 即一页定义为2^13,也就是8KB
static const size_t PAGE_SHIFT = 13;

//条件编译,为了适应不同平台
#ifdef _WIN64
typedef unsigned long long PAGE_ID;
#elif _WIN32
typedef size_t PAGE_ID;//页号
#endif

#ifdef _WIN32
#include <windows.h>
#endif // _WIN32
//系统调用接口,直接向系统堆申请k页内存,脱离malloc函数
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
	void* ptr = VirtualAlloc(0, kpage * (1 << PAGE_SHIFT),
		MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
	// brk mmap等
#endif
	if (ptr == nullptr)
		throw std::bad_alloc();
	return ptr;
}
//系统调用接口,直接向系统堆释放内存,脱离free
inline static void SystemFree(void* ptr)
{
#ifdef _WIN32
	VirtualFree(ptr, 0, MEM_RELEASE);
#else
	// sbrk unmmap等
#endif
}


//obj对象的前4个或者8个字节,存放的是obj的下一个对象的地址,相当于obj->next
static void*& NextObj(void* obj)
{
	return *(void**)obj;
}


//管理切分好的小对象的自由链表
struct FreeList
{
public:
	void Push(void* obj)
	{
		assert(obj);
		//头插
		NextObj(obj) = _freeList;
		_freeList = obj;

		_size++;
	}

	void PushRange(void* start,void* end,size_t n)
	{
		assert(start);
		assert(end);

		NextObj(end) = _freeList;
		_freeList = start;

		调试技巧:
		1、条件断点
		2、调用堆栈
		3、中断程序
		//int i = 0;
		//while (start)
		//{
		//	start = NextObj(start);
		//	i++;
		//}
		//if (i != n)
		//{
		//	int x = 0;
		//}

		_size += n;
	}

	//n表示删除了多少个,这里的删除并不是真的把空间释放了,而是把小对象从自由链表中解掉
	//即可,删除掉的这些小对象本质也是被外面调用方函数拿到了的,注意这里start和end是引用
	//输出型参数
	void PopRange(void*& start, void*& end, size_t n)
	{
		assert(n <= _size);

		start = _freeList;
		end = start;

		//end走n-1步是为了修改end的next和更新_freeList
		for (size_t i = 0; i < n - 1; i++)
		{
			end = NextObj(end);
		}
		_freeList = NextObj(end);
		NextObj(end) = nullptr;//一定要注意置空
		_size -= n;
	}

	//这里的删除需要同时返回这个小对象
	void* Pop()
	{
		assert(_freeList);
		//头删
		void* obj = _freeList;
		_freeList = NextObj(obj);

		_size--;

		return obj;
	}

	size_t& MaxSize()
	{
		return _maxSize;
	}

	size_t Size()
	{
		return _size;
	}

	bool Empty()
	{
		return _freeList == nullptr;
	}

public:
	void* _freeList = nullptr;//自由链表
private:
	size_t _maxSize = 1;
	size_t _size = 0;
};


计算对象大小的对齐映射规则
class SizeClass
{
public:
	// 整体控制在最多10%左右的内碎片浪费
	// [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)

	//向上对齐计算对象大小
	//由于这里是按照一定规则对齐的,所以其实是会有内存碎片的(内碎片),即申请1字节也是给8字节,申请2字节
	//也是给8字节的,但是根据以上计算可以控制最多10%左右的内碎片浪费。
	static inline size_t RoundUp(size_t bytes)
	{
		if (bytes <= 128)
		{
			return _RoundUp(bytes, 8);
		}
		else if (bytes <= 1024)
		{
			return _RoundUp(bytes, 16);
		}
		else if (bytes <= 8*1024)
		{
			return _RoundUp(bytes, 128);
		}
		else if (bytes <= 64*1024)
		{
			return _RoundUp(bytes, 1024);
		}
		else if (bytes <= 256*1024)
		{
			return _RoundUp(bytes, 8 * 1024);
		}
		else
		{
			//以页为单位对齐
			return _RoundUp(bytes, 1 << PAGE_SHIFT);
		}
	}

	//计算某一对象映射的哈希桶的下标
	static inline size_t Index(size_t alignSize)
	{
		assert(alignSize <= MAX_BYTES);

		//不同的对齐数对应的区间中有多少个哈希桶
		static int group[] = { 16,56,56,56 };

		if (alignSize <= 128)
		{
			return _Index(alignSize, 3);
		}
		else if (alignSize <= 1024)
		{
			return _Index(alignSize - 128, 4) + group[0];
		}
		else if (alignSize <= 8 * 1024)
		{
			return _Index(alignSize - 1024, 7) + group[0] + group[1];
		}
		else if (alignSize <= 64 * 1024)
		{
			return _Index(alignSize - 8 * 1024, 10) + group[0] + group[1] + group[2];
		}
		else if (alignSize <= 256 * 1024)
		{
			return _Index(alignSize - 64 * 1024, 13) + group[0] + group[1] + group[2] + group[3];
		}
		else
		{
			//alignSize太大就无法映射到哈希桶中了,这时应该直接断言错误即可
			assert(false);
			return -1;
		}
	}

	//计算当有线程申请一个size大小的内存块时,threadcache对应哈希桶没有内存块
	//时一次向central cache申请多少个size大小的内存块,多的挂到threadcache
	//对应的哈希桶中,这个是大佬们研究之后得出来的算法
	static size_t NumMoveSize(size_t size)
	{
		int n = MAX_BYTES / size;
		if (n < 2)
		{
			return 2;
		}
		else if (n > 512)
		{
			return 512;
		}
		else
		{
			return n;
		}
	}

	//计算当CentralCache对应位置的哈希桶中没有空闲的span的时候向PageCache
	//申请多少页大小的span对象,这个是别人通过测试得出来的算法
	static size_t NumMovePage(size_t size)
	{
		size_t num = NumMoveSize(size);
		size_t npage = num * size;
		npage >>= PAGE_SHIFT;
		if (npage == 0)
		{
			npage = 1;
		}
		return npage;
	}

private:
	static inline size_t _RoundUp(size_t bytes, size_t alignNum/*对齐数*/)
	{
		//对齐之后的大小
		//size_t alignSize;
		//if (bytes % alignNum == 0)
		//{
		//	alignSize = bytes;
		//}
		//else
		//{
		//	alignSize = (bytes / alignNum + 1) * alignNum;
		//}
		//return alignSize;

		return (bytes + alignNum - 1) & ~(alignNum - 1);
	}

	//static inline size_t _Index(size_t alignSize, size_t align)
	//{
	//	int index;
	//	if (alignSize % (1 << align) != 0)
	//	{
	//		index = alignSize / align;
	//	}
	//	else
	//	{
	//		index = alignSize / align - 1;
	//	}
	//	return index;
	//}

	static inline size_t _Index(size_t bytes, size_t align_shift)
	{
		return ((bytes + (1 << align_shift) - 1) >> align_shift) - 1;
	}
};

//以页为单位的大块内存,1页=8k=8*1024字节
struct Span
{
	PAGE_ID _pageId = 0;//Span大块内存的起始页号,页号相当于一个地址,描述这个大块内存的位置
	size_t _n = 0;//页数,代表这个Span有多少页内存,表示这个Span的大小

	Span* _next = nullptr;//用双向链表结构组织起来
	Span* _prev = nullptr;

	void* _freeList = nullptr;//用Span切好的小块内存的自由链表
	size_t _useCount = 0;//记录Span切好的小块内存被使用的数量,方便后续归还内存

	size_t _objSize = 0;//记录该span被切成的小块内存的大小,让释放内存时不用传大小

	//表示该span是否正在被使用,如果span在PageCache,则认为该span没有被使用
	//可以在PageCache和前后的空闲页合并,如果span被分到CentralCache,则说明
	//该span被使用,不能被合并
	bool _isUse = false;

};

//用带头双向链表结构把多个Span组织起来
class SpanList
{
public:
	SpanList()
	{
		//哨兵位的头节点
		_head = new Span;
		_head->_next = _head;
		_head->_prev = _head;
	}

	void PushFront(Span* newSpan)
	{
		Insert(Begin(), newSpan);
	}

	bool Empty()
	{
		return _head->_next == _head;
	}

	Span* PopFront()
	{
		Span* front = Begin();
		Erase(Begin());
		return front;
	}

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

	Span* End()
	{
		return _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;

	}
	void Erase(Span* pos)
	{
		assert(pos != _head);

		Span* prev = pos->_prev;
		Span* next = pos->_next;

		prev->_next = next;
		next->_prev = prev;
	}
private:
	Span* _head;

public:
	//桶锁,因为全局只有一个central cache,如果有多个线程同时访问同一个桶的时候
	//会有线程安全的问题,所以需要加锁,但是多个线程同时访问到同一个桶的概率不大
	//所以锁竞争没有那么激烈
	std::mutex _mtx;
};
相关推荐
q56731523几秒前
在 Bash 中获取 Python 模块变量列
开发语言·python·bash
JSU_曾是此间年少12 分钟前
数据结构——线性表与链表
数据结构·c++·算法
许野平25 分钟前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
也无晴也无风雨29 分钟前
在JS中, 0 == [0] 吗
开发语言·javascript
狂奔solar37 分钟前
yelp数据集上识别潜在的热门商家
开发语言·python
写bug的小屁孩1 小时前
前后端交互接口(三)
运维·服务器·数据库·windows·用户界面·qt6.3
朱一头zcy1 小时前
C语言复习第9章 字符串/字符/内存函数
c语言
此生只爱蛋1 小时前
【手撕排序2】快速排序
c语言·c++·算法·排序算法
blammmp1 小时前
Java:数据结构-枚举
java·开发语言·数据结构
何曾参静谧1 小时前
「C/C++」C/C++ 指针篇 之 指针运算
c语言·开发语言·c++