【高性能内存池】central cache 4

central cache

  • [1 central cache框架结构](#1 central cache框架结构)
  • [2 span结构](#2 span结构)
    • [2.1 _pageId](#2.1 _pageId)
    • [2.2 _n](#2.2 _n)
    • [2.3 _next和_prev](#2.3 _next和_prev)
    • [2.4 _objSize](#2.4 _objSize)
  • [3 spanList结构](#3 spanList结构)
    • [3.1 spanList的桶锁](#3.1 spanList的桶锁)
    • [3.2 central cache单例模式 - 饿汉模式](#3.2 central cache单例模式 - 饿汉模式)
  • [4 慢开始反馈调节算法](#4 慢开始反馈调节算法)
    • [4.1 NumMoveSize()函数](#4.1 NumMoveSize()函数)
    • [4.2 _maxSize的作用](#4.2 _maxSize的作用)
    • [4.3 具体过程](#4.3 具体过程)
  • [5 thread cache从central cache申请内存的过程](#5 thread cache从central cache申请内存的过程)
    • [5.1 thread cache中FetchFromCentralCache函数实现](#5.1 thread cache中FetchFromCentralCache函数实现)
    • [5.2 central cache中GetOneSpan函数实现](#5.2 central cache中GetOneSpan函数实现)
    • [5.3 central cache中FetchRangeObj函数实现](#5.3 central cache中FetchRangeObj函数实现)
  • [6 总结](#6 总结)

当申请的内存小于256KB的时候直接去thread cache中取,如果申请内存大于256KB或者thread cache中没有足够的内存时,就要去central cache中拿

1 central cache框架结构

central cache thread cache拥有一样的哈希桶的结构,好处是在thread cache要从central cache中取内存时,可以直接取。

central cache的结构如下:

central cache的每一个桶中挂的是span,这个span是什么下面会讲解。而span里面挂的才是切好的内存块freeList.

每个span管理的都是一个以页为单位的大块内存,每个桶里面的若干span是按照双链表的形式链接起来的,并且每个span里面还有一个自由链表,这个自由链表里面挂的就是一个个切好了的内存块,根据其所在的哈希桶这些内存块被切成了对应的大小。

2 span结构

1.span是以页为单位的大内存块 2.span是双向链表 3.span里面挂着freeList作为成员

根据上面的内存,编写span的结构:

cpp 复制代码
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;          // 是否在被使用
};

2.1 _pageId

每个程序运行起来后都有自己的进程地址空间,在32位平台下,进程地址空间的大小是2^32^;而在64位平台下,进程地址空间的大小就是2^64^。

页的大小一般是4K或者8K,我们以8K为例。

在32位平台下,进程地址空间就可以被分成 2^32^ / 2^13^ = 2^19^个页;

在64位平台下,进程地址空间就可以被分成 2^64^ / 2^13^ = 2^51^个页。

页号本质与地址是一样的,它们都是一个编号,只不过地址是以一个字节为一个单位,而页是以多个字节为一个单位。

由于页号在64位平台下的取值范围是,因此我们不能简单的用一个无符号整型来存储页号,这时我们需要借助条件编译来解决这个问题。

cpp 复制代码
#ifdef _WIN64
	typedef unsigned long long PAGE_ID;
#elif _WIN32
	typedef size_t PAGE_ID;
#else
	//linux
#endif

需要注意的是,在32位下,_WIN32有定义,_WIN64没有定义;而在64位下,_WIN32_WIN64都有定义。因此在条件编译时,我们应该先判断_WIN64是否有定义,再判断_WIN32是否有定义。

2.2 _n

span管理以页为单位的大块内存,至于一个span管理的到底是多少个页,这不确定,因此,需要用_n来记录一下。

2.3 _next和_prev

就和下面的结构是一样的

cpp 复制代码
struct TreeNode
{
public:
	TreeNode* _prev;
	TreeNode* _next;
};

每一个节点都有它的前驱节点和后继节点,方便链接起来成为一个链表。

2.4 _objSize

这个表示切好的小对象的大小

3 spanList结构

对于一个带头双向循环链表,主要的操作就是头插,头删,任意位置插入,任意位置删除。

这里就不作多的解释,都比较简单,直接列出代码:

cpp 复制代码
class SpanList
{
public:
	SpanList()
	{
		_head = new Span;
		_head->_next = _head;
		_head->_prev = _head;
	}

	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 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);

		// 1、条件断点
		// 2、查看栈帧
		/*if (pos == _head)
		{
		int x = 0;
		}*/

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

		prev->_next = next;
		next->_prev = prev;
	}

private:
	Span* _head;
public:
	std::mutex _mtx; // 桶锁
};

3.1 spanList的桶锁

thread cache是每个线程都有一份,所以不需要加锁。而central cache是线程共有的,所以得加锁。在central cache中有很多spanList,也就是有很多桶。当不同thread cache访问不同的桶时,不需要加桶锁,当不同的thread cache访问相同的桶时,需要加桶锁。

3.2 central cache单例模式 - 饿汉模式

central cache在整个进程中只有一个,对于这种只能创建一个对象的类,我们可以将其设置为单例模式。

单例模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。单例模式又分为饿汉模式和懒汉模式,懒汉模式相对较复杂,我们这里使用饿汉模式就足够了。

cpp 复制代码
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;
};

CentralCache的构造函数和拷贝构造函数设置为私有,在C++11中也可以在函数声明的后面加上=delete进行修饰。

CentralCache类当中还需要有一个CentralCache类型的静态的成员变量,当程序运行起来后我们就立马创建该对象,在此后的程序中就只有这一个单例了。

4 慢开始反馈调节算法

thread cachecentral cache申请内存时,central cache应该给出多少个对象呢?这是一个值得思考的问题,如果central cache给的太少,那么thread cache在短时间内用完了又会来申请;但如果一次性给的太多了,可能thread cache用不完也就浪费了。

鉴于此,我们这里采用了一个慢开始反馈调节算法。当thread cachecentral cache申请内存时,如果申请的是较小的对象,那么可以多给一点,但如果申请的是较大的对象,就可以少给一点。

4.1 NumMoveSize()函数

通过下面这个函数,我们就可以根据所需申请的对象的大小计算出具体给出的对象个数,并且可以将给出的对象个数控制到2~512个之间。也就是说,就算thread cache要申请的对象再小,我最多一次性给出512个对象;就算thread cache要申请的对象再大,我至少一次性给出2个对象。

cpp 复制代码
	//static const size_t MAX_BYTES = 256 * 1024;
	// 一次thread cache从中心缓存获取多少个
	static size_t NumMoveSize(size_t size)
	{
		assert(size > 0);

		// [2, 512],一次批量移动多少个对象的(慢启动)上限值
		// 小对象一次批量上限高
		// 小对象一次批量上限低
		//size越大(申请的越大) --> num越小(获得的越少)
		int num = MAX_BYTES / size;
		if (num < 2)
			num = 2;

		if (num > 512)
			num = 512;

		return num;
	}

这里代码为什么要这么设计?

假设申请的内存很大,size = 256KB,一次给512个,总共就是256 * 512 KB.

假设申请的内存比较小,size = 256B,一次给2个,总共就是256 * 2B.

可以发现差距很大。
于是为了起到一个平衡的作用,让申请size很大的内存时,不给太多;申请size很小的内存时,不给太少。就对申请个数的上界和下界进行了限制。申请的内存如果特别大,那么不能申请低于2个。申请的内存如果特别小,那么不能申请超过512个。
申请的特别多的就少给点(一次性不能给太多,会造成浪费),申请的特别少的就多给点(也不能给太少,不然会频繁的到central cache中要)。

同时,还要在freeList中增加一个成员变量_maxSize,并提供一个接口获得_maxSize.

4.2 _maxSize的作用

cpp 复制代码
class FreeList
{
public:
	size_t& MaxSize()
	{
		return _maxSize;
	}
private:
	void* _freeList = nullptr;
	size_t _maxSize = 1;   //这里是新添的_maxSize
	size_t _size = 0;
};

_maxSize的作用就是调节。_maxSize表示最大能分配的FreeList的个数。如果每次要申请的个数都是最大个数(_maxSize),那么_maxSize+1。这样,随着申请的次数变多,_maxSize变大,每次能申请到的个数也就变多了。这就是慢增长的过程。

4.3 具体过程

cpp 复制代码
void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{
	// 慢开始反馈调节算法  -- 小对象给多一点,大对象给少一点
	//达到的目的:
	//1.最开始不会一次向central cache一次批量要太多,因为要太多用不完
	//2.如果你不要这个size大小内存需求,那么batchNum就会不断增长,直到上限
	//3.size越大,一次向central cache要的batchNum就越小
	//4.size越小,一次向central cache要的batchNum就越大(慢慢增长的)
	size_t batchNum;
	if (_freeLists[index].MaxSize() < SizeClass::NumMoveSize(size))
		batchNum = _freeLists[index].MaxSize();
	else
		batchNum = SizeClass::NumMoveSize(size);
	//size_t batchNum = std::min(_freeLists[index].MaxSize(), SizeClass::NumMoveSize(size));
	//头文件windows.h中有一个宏也叫min,min是函数模板,所以直接用windows.h中的了。所以会报错


	//慢增长
	if (batchNum == _freeLists[index].MaxSize())
	{
		_freeLists[index].MaxSize() += 1;
	}

	//未完...
}

5 thread cache从central cache申请内存的过程

回顾之前thread cache申请内存的代码.

cpp 复制代码
void* ThreadCache::Allocate(size_t size)
{
	assert(size <= MAX_BYTES);          
	size_t alignSize = SizeClass::RoundUp(size);   //alignSzie 表示最终内存给的字节数
	size_t index = SizeClass::Index(size);         //index表示内存块在数组中的索引

	if (!_freeLists[index].Empty())
	{
		return _freeLists[index].Pop();
	}
	else
	{
		//没有内存块,就去centrleCache获取对象
		return FetchFromCentralCache(index, alignSize);
	}
}

thread cache中没有足够的内存时,会去central cache中获取对象。

5.1 thread cache中FetchFromCentralCache函数实现

根据慢开始反馈调节算法最终得到central cache会给thread cachefreeList的个数,(要根据central cache的具体情况返回最终实际会给thread cachefreeList的个数。这个后面再详细讲解。)这时候就要根据得到的不同的freeList的个数来进行判断了:

1.如果只得到1个freeList,那么直接拿去用,返回freeList的起始地址即可。

2.如果得到了很多的freeList,那么要将得到的多的freeList继续挂载在原来的thread cache的哈希桶中。(这样下次thread cache再申请内存时,不会直接到central cache中去要,而是优先在thread cache中拿)

根据这样的思路,编写代码:

cpp 复制代码
void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{
	// 慢开始反馈调节算法  -- 小对象给多一点,大对象给少一点
	//达到的目的:
	//1.最开始不会一次向central cache一次批量要太多,因为要太多用不完
	//2.如果你不要这个size大小内存需求,那么batchNum就会不断增长,直到上限
	//3.size越大,一次向central cache要的batchNum就越小
	//4.size越小,一次向central cache要的batchNum就越大(慢慢增长的)
	size_t batchNum;
	if (_freeLists[index].MaxSize() < SizeClass::NumMoveSize(size))
		batchNum = _freeLists[index].MaxSize();
	else
		batchNum = SizeClass::NumMoveSize(size);
	//size_t batchNum = std::min(_freeLists[index].MaxSize(), SizeClass::NumMoveSize(size));
	//头文件windows.h中有一个宏也叫min,min是函数模板,所以直接用windows.h中的了。所以会报错
	void* start = nullptr;
	void* end = nullptr;

	//慢增长
	if (batchNum == _freeLists[index].MaxSize())
	{
		_freeLists[index].MaxSize() += 1;
	}

	//actualNum表示实际给的freelist的个数,batchNum表示thread chache要的freelist的个数
	//这里后续讲解
	size_t actualNum = CentralCache::GetInstance()->FetchRangeObj(start, end, batchNum, size);
	assert(actualNum > 0);

	//如果实际只获取到1个
	if (actualNum == 1)
	{
		assert(start == end);
		return start;
	}
	else
	{
		//如果获取到多个,就应该把获取到的自由链表的头节点返回去
		_freeLists[index].PushRange(NextObj(start), end, actualNum-1);
		return start;
	}
}

如何将多的freeList挂载到thread cache中的哈希桶(_freeList)中?

下面是FreeList的结构,新增了PushRange函数。

cpp 复制代码
class FreeList
{
public:
	void PushRange(void* start, void* end, size_t n)
	{
		NextObj(end) = _freeList;  //相当于得到的新freeList的end的next指向_freeList
		_freeList = start;  //_freeList作为新的头节点
		_size += n;
	}

private:
	void* _freeList = nullptr;
	size_t _maxSize = 1;
	size_t _size = 0;
};

上面代码的绘图过程如下:

5.2 central cache中GetOneSpan函数实现

central cache中写一个函数GetOneSpan获得一个非空的Span.

函数头的设计:

cpp 复制代码
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)

传入一个SpanListsize。主要功能就是在SpanList中找,遇到非空的Span就直接返回。

cpp 复制代码
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;
		}
	}

	// 先把central cache的桶锁解掉,这样如果其他线程释放内存对象回来,不会阻塞
	list._mtx.unlock();
	return span;
}

5.3 central cache中FetchRangeObj函数实现

这个函数的作用就是从central cache中获取一定数量的自由链表给thread cache.

主要流程是:根据计算出的indexspanList中找到spanList[index],在里面找到一个非空的span,然后从非空的span中取出freeList,根据之前计算出的batchNum的个数计算出这个非空的span能取出多少个freeList(也就是真实能取出多少个freeList,用actualNum表示)。如果
actualNum小于batchNum,就有多少取多少。

cpp 复制代码
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
	//这里调用GetOneSpan时是持有锁的,所以在GetOneSpan中有一个解锁的过程
	Span* span = GetOneSpan(_spanLists[index], size);
	assert(span);
	assert(span->_freeList);

	// 从span中获取batchNum个对象
	// 如果不够batchNum个,有多少拿多少
	start = span->_freeList;
	end = start;
	size_t i = 0;
	size_t actualNum = 1;
	while ( i < batchNum - 1 && NextObj(end) != nullptr)
	{
		end = NextObj(end);
		++i;
		++actualNum;
	}
	span->_freeList = NextObj(end);
	NextObj(end) = nullptr;
	span->_useCount += actualNum;

	 条件断点
	int j = 0;
	void* cur = start;
	while (cur)
	{
		cur = NextObj(cur);
		++j;
	}

	if (j != actualNum)
	{
		int x = 0;
	}

	//解锁
	_spanLists[index]._mtx.unlock();

	return actualNum;
}

6 总结

  1. 讲解了central cache的结构
  2. 讲了spanList和span的数据结构,和一些接口
  3. 讲解了thread cache从central cache中申请内存的过程

相关推荐
可均可可30 分钟前
C++之OpenCV入门到提高004:Mat 对象的使用
c++·opencv·mat·imread·imwrite
白子寰1 小时前
【C++打怪之路Lv14】- “多态“篇
开发语言·c++
小芒果_011 小时前
P11229 [CSP-J 2024] 小木棍
c++·算法·信息学奥赛
XuanRanDev1 小时前
【每日一题】LeetCode - 三数之和
数据结构·算法·leetcode·1024程序员节
gkdpjj1 小时前
C++优选算法十 哈希表
c++·算法·散列表
代码猪猪傻瓜coding1 小时前
力扣1 两数之和
数据结构·算法·leetcode
王俊山IT1 小时前
C++学习笔记----10、模块、头文件及各种主题(一)---- 模块(5)
开发语言·c++·笔记·学习
-Even-1 小时前
【第六章】分支语句和逻辑运算符
c++·c++ primer plus
我是谁??2 小时前
C/C++使用AddressSanitizer检测内存错误
c语言·c++
南宫生2 小时前
贪心算法习题其三【力扣】【算法学习day.20】
java·数据结构·学习·算法·leetcode·贪心算法