central cache设计及实现

news2024/9/24 5:26:20

在这里插入图片描述

你好,我是安然无虞。

central cache 设计及实现

central cache 也是一个哈希桶结构, 而且它的哈希桶的映射关系跟 thread cache 是一样的, 不同的是 central cache 每个哈希桶位置上挂的是 SpanList 双向链表结构, 而且每个哈希桶下面的 span 中的大块内存被按映射关系切成了一个个小的内存块对象挂在 span 的自由链表中.

在这里插入图片描述
当 thread cache 中没有内存时, 就会批量地向 central cache 申请一些内存对象, 这里的批量获取内存对象采用慢开始反馈调节算法, central cache 也有一个哈希映射的 SpanList, SpanList 中挂着 span, span 是管理以页为单位的大块内存, 从 span 中取出对象给 thread cache, 这个过程是需要加锁的, 因为可能多个 thread cache 同时访问 central cache 映射的同一个 SpanList, 不过 central cache 使用的是桶锁, 这样就尽可能做到了提高效率.

当 central cache 映射的 SpanList 中的所有 span 都没有内存以后, 则需要向 page cache 申请一个新的 span 对象, 拿到 span 以后将其管理的内存按照特定大小切好作为自由链表链接到一起, 然后从 span 中取出对象给 thread cache.

thread cache与central cache的不同:

thread cache 与 central cache 的不同之处主要有两点, 第一点在于每个线程通过 TLS 都获取自己专属的 thread cache, 也就是说每个线程都有一个 thread cache, 所以线程从 thread cache 里面申请内存是不需要加锁的, 这样就有效地避免了锁竞争的问题. 但是central cache 在全局只有一份, 也就是说每个 thread cache 向同一个 central cache 申请内存, 所以 central cache 是需要加锁的.

但是 central cache 在加锁时并不是将其全部锁上, central cache在加锁时用的是桶锁, 也就是说每个桶都有一个锁, 此时只有当多个线程同时访问 central cache 的同一个桶时才会存在锁竞争, 如果是多个线程同时访问 central cache 的不同桶就不会存在锁竞争.

第二个不同点在于 thread cache 每个桶中挂的是切好的小块内存的自由链表, 而 central cache 每个桶中挂的是一个个 span, span 是管理以页为单位的大块内存, 而且每个桶里面的若干个 span 是按照双向链表的形式链接起来的, 并且每个 span 里面还有一个自由链表, 根据其所在的位置这些 span 被切成了对应的大小.


span的定义

span 是管理以页为单位的大块内存.

// Span 管理一个以页为单位的大块内存
struct Span
{
	PAGE_ID _pageId = 0; // 大块内存起始页的页号
	size_t _n = 0; // 页的数量

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

	void* _freeList = nullptr; // 切好的小块内存的自由链表
};
  • _pageId: 对于 span 管理的以页为单位的大块内存, 我们需要知道这块内存具体在什么位置, 方便后面的 page cache 进行前后页的合并, 因此在 span 的定义中要有大块内存起始页的页号.
  • _n: 每个 span 管理的页数都是不一样的, 它受到很多因素的影响, 所以在 span 的定义中要记录页的数量.
  • _prev, _next: 之所以采用的是带头双向循环链表, 是为了很方便的将需要从 page cache 中移除的 span 移除出去, 如若采用的是单链表, 那么在删除时需要保留当前节点的前一个节点.
  • _freeList: 每个 span 管理的大块内存, 都会被切成相应大小的内存对象挂到当前 span 的自由链表中, 比如16Byte哈希桶中的span, 会被切成一个个16Byte大小的内存对象挂到当前 span 的自由链表中, 因此在 span 结构中还需要存储切好的小块内存的自由链表.

其中还需要注意的是大块内存起始页的页号, 它在 32位平台和64位平台 是不一样的, 假设一个页的大小是 8KB 的话, 在32位平台, 有 2^32 / 2^13 = 2^19 个页, 4字节数据类型可以存的下, 但是在64位平台, 有 2^ 64 / 2 ^ 13 = 2 ^ 51 个页, 这个时候4字节数据类型就存不下了, 需要 long long 这种8个字节的数据类型.

所以要有如下定义:

#ifdef _WIN64
typedef unsigned long long PAGE_ID;
#elif _WIN32
	typedef size_t PAGE_ID;
#endif

还有一个小细节需要注意的是, 关于_WIN32宏 和 _WIN64宏 的差异, _WIN32宏 在32位和64位程序都有定义,且总是定义的, 但是 _WIN64宏 只有在64位程序下才有定义. 所以为了区分当前程序是32位还是64位, 需要将 _WIN64 放在前面.


SpanList的定义

SpanList 是一个带头双向循环链表的结构.

// 带头双向循环循环链表
class SpanList
{
public:
	SpanList()
	{
		_head = new Span;
		_head->_prev = _head;
		_head->_next = _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);
		assert(pos != _head);

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

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


private:
	Span* _head = nullptr;

public:
	std::mutex _mtx; // 桶锁
};

因为我们之前在数据结构中学过这个, 所以就显得很简单了.

对了, 还需要注意的是, 从双向链表中删除的 span 会还给 page cache, 所以不需要进行 delete 操作.


Central Cache代码框架

线程通过 TLS 来实现每个线程都有一个自己专属的 thread cache, 但是 central cache 和 page cache 全局只有一个, 对于这种只能创建一个对象的类, 我们将其封装成单例模式.

// 单例模式:饿汉模式
class CentralCache
{
public:
	// 提供一个全局访问点
	static CentralCache* GetInstance()
	{
		return &_sInst;
	}

	// 从中心缓存获取一定数量的对象给thread cache
	size_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t byte_size);

	// 从SpanList或者page cache获取一个非空的Span
	Span* GetOneSpan(SpanList& list, size_t byte_size);

private:
	SpanList _spanList[NFREELISTS]; // 按照对齐方式映射 - 这点与thread cache很相似

private:
	CentralCache() // 构造函数私有
	{}

	CentralCache(const CentralCache&) = delete; // 防止拷贝

	static CentralCache _sInst;
};

因为要保证CentralCache类只能实例化出一个对象, 所以要将其构造函数私有化以及防止拷贝.

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

CentralCache CentralCache::_sInst;

慢开始反馈调节算法

thread cache 一次向 central cache 批量多少个内存对象合适呢, 这个问题值得我们思考, 因为一次给少了, 需要多次申请, 这样效率就低了, 但是如果一次给多了, 又会导致浪费.

所以这里想了一个折中的办法, 采用慢启动反馈调节算法, 也就是说, 对于小对象一次批量上限高, 对于大对象一次批量上限低.

//thread cache 一次从中心缓存获取多少个内存对象
static size_t NumMoveSize(size_t size)
{
	assert(size > 0);

	// [2, 512], 一次批量获取多少个对象的(慢启动)上限值
	// 小对象一次批量上限高
	// 大对象一次批量上限低
	size_t num = MAX_BYTES / size;
	if (num < 2)
		num = 2;

	if (num > 512)
		num = 512;

	return num;
}

也就是说, 虽然小对象一次批量给的多, 但是不会超过512个, 大对象一次批量给的少, 但是不会低于2个, 所以一次批量给的对象个数控制在2~512个之间.

不过即使申请的是小对象, 一次性给出512个也是很多的, 所以我们可以在 FreeList 结构中增加一个叫做 _maxSize 的成员变量, 该变量的初始值设置为1, 并且提供一个成员函数用于获取这个变量. 也就是说, 现在 thread cache 中的每个自由链表都会有一个自己的_maxSize.

//管理切分好的小对象的自由链表
class FreeList
{
public:
	size_t& MaxSize()
	{
		return _maxSize;
	}

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

这样下来当 thread cache 申请对象时, 我们会比较 _maxSize 和慢开始反馈调节算法计算出来的值, 取其中的较小值作为本次申请对象的个数. 此外, 如果本次采用的是 _maxSize 的值, 那么还会将 thread cache 中该自由链表的 _maxSize 的值加一.

所以, thread cache 第一次向 central cache 申请某大小的对象时, 申请到的都是一个, 但是该自由链表中的 _maxSize 增加了,所以下一次 thread cache 再向 central cache 申请同样大小的对象时, 最终就会申请到两个, 直到该自由链表中_maxSize的值增长到超过慢开始反馈调节算法计算出的值后就不会继续增长了, 此后申请到的对象个数就是计算出的个数.


从中心缓存获取对象

thread cache 向 central cache 获取内存对象时, 都是通过慢开始反馈调节算法计算出需要申请对象的个数, 然后再向 central cache 申请.

如果申请到的对象只有一个, 直接返回即可, 如果申请到的对象有多个, 那么需要在返回第一个对象之前, 需要将除第一个对象之外的所有对象头插到 thread cache 对应的哈希桶的自由链表中.

//从中心缓存获取对象
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 = std::min(_freeLists[index].MaxSize(), SizeClass::NumMoveSize(size));
	if (_freeLists[index].MaxSize() == batchNum)
		_freeLists[index].MaxSize() += 1;

	void* start = nullptr;
	void* end = nullptr;
	size_t actualNum = CentralCache::GetInstance()->FetchRangeObj(start, end, batchNum, size);
	assert(actualNum > 0);

	if (actualNum == 1) // 申请到的对象只有一个, 直接返回即可
	{
		assert(start == end);
		return start; 
	}
	else // 申请到的对象有多个, 还需要将剩下的对象挂到thread cache对应的哈希桶中
	{
		_freeLists[index].PushRange(NextObj(start), end); 
		return start;
	}
}

从中心缓存获取一定数量的对象

我们需要从 central cache 中获取 batchNum 个指定大小的对象, 这些对象肯定是从 central cache 中指定的哈希桶中的 span 里面取出来的, 由于这 batchNum 个对象是链接在一起的, 所以我们只需要知道其头和尾即可, 故而这里采用输出型参数.

// 从中心缓存获取一定数量的对象给thread cache
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t byte_size)
{
	size_t index = SizeClass::Index(byte_size);

	_spanList[index]._mtx.lock();

	// 从SpanList或者page cache获取一个非空的span
	Span* span = GetOneSpan(_spanList[index], byte_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; //将取出的一段链表的表尾置空

	_spanList[index]._mtx.unlock();

	return actualNum;
}

因为 central cache 是被所有线程共享的, 所以当我们访问 central cache 的哈希桶时, 先对其进行加锁, 等到获得内存对象后, 再对其进行解锁.

首先我们要从 central cache 的对应位置的哈希桶中获取一个非空的 span, 然后从这个非空的 span 里面取出 batchNum 个内存对象即可, 如果没有 batchNum 个, 那么就有多少取多少.

需要说明的是, 虽然我们实际申请到的对象个数可能会比 batchNum 小, 但是没有关系. 因为本身 thread cache 的就是向 central cache 申请一个对象, 这里之所以要一次申请多个对象, 是因为这样一来下次线程再申请相同大小的对象时就可以直接在 thread cache 里面获取了, 而不用再向 central cache 申请对象, 因为向 central cache 申请对象可能会有加锁解锁问题.

头插一段范围的对象到自由链表

前面我们说到, 如果从 central cache 中申请到的对象不止一个, 除了第一个对象被返回外, 其余所有对象全部被头插进 thread cache 对应位置的自由链表中.

// 管理切分好的小块内存的自由链表
class FreeList
{
public:
	// 头插一段范围的内存对象
	void PushRange(void* start, void* end)
	{
		assert(start);
		assert(end);
		
		NextObj(end) = _freeList;
		_freeList = start;
	}

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/351029.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

大数据之数据中台

目录数据仓库传统数据仓库无法支撑互联网时代的商业智能数据埋点数据仓库 数据仓库是在企业管理和决策中面向主题的、集成的、与时间相关的、不可修改的数据集合。 在电商场景中&#xff0c;有一个数据库专门存放订单的数据&#xff0c;另外一个数据库存放会员相关的数据。构建…

订单服务:订单流程

订单流程 订单流程是指从订单产生到完成整个流转的过程&#xff0c;从而行程了一套标准流程规则。而不同的产品类型或业务类型在系统中的流程会千差万别&#xff0c;比如上面提到的线上实物订单和虚拟订单的流程&#xff0c;线上实物订单与 O2O 订单等&#xff0c;所以需要根据…

[Datawhale][CS224W]图机器学习(三)

目录一、简介与准备二、教程2.1 下载安装2.2 创建图2.2.1 常用图创建&#xff08;自定义图创建&#xff09;1.创建图对象2.添加图节点3.创建连接2.2.2 经典图结构1.全连接无向图2.全连接有向图3.环状图4.梯状图5.线性串珠图6.星状图7.轮辐图8.二项树2.2.3 栅格图1.二维矩形栅格…

如何修改 类名::静态变量?

好久没更新了&#xff0c;其中经历了备战省赛&#xff0c;重装电脑&#xff0c;换服务器&#xff0c;重新搭建博客等一系列事&#xff0c;受到许多技术大牛的指点&#xff0c;而且新人太厉害了&#xff0c;卷卷卷&#xff01; Geek Challenge 2021有一道BabyPOP&#xff0c;这…

vue写一个翻页的时间插件

读秒效果有一个从上向下的翻页效果 效果图 clock代码 <template><div class"dateClock"><div class"todayClass"><p class"datep">{{dateToday}}</p><span style"float:right;font-size:16px">…

基于 VPX 总线的工件台运动控制系统研究与开发-DSP+FPGA硬件架构(一)

作为光刻机核心单元之一&#xff0c;超精密工件台主要负责实现快速扫描、上下片、精密定位、调平调焦等功能。目前&#xff0c;较为成熟的方案大多采用 VME 并行总线架构来建立超精密工件台控制系统&#xff0c;由于随着系统性能要求的提升&#xff0c;VME 总线以及相应的处理器…

最大连续子列和

给定一个数组&#xff0c;求它的最大连续子列和。这个问题有四种解法。 1、暴力循环(O(n^3))分析这个问题&#xff0c;既然是子列&#xff0c;那么它最长为n&#xff0c;最短为1。要想求和我们一般需要知道这个子列的左端下标和右端下标&#xff0c;再求这个子列的和。最简单的…

深入浅出带你学习Nginx中间件常见漏洞

前言 上篇文章总结了APACHE中间件的常见漏洞&#xff0c;本文介绍一个同样很常用的中间件——nginx&#xff0c;本文会讲解关于nginx中间件的常见漏洞利用方式与危害&#xff0c;下面展开来给大家讲解。 Nginx是什么&#xff1f; 在讲漏洞之前我们需要先知道什么是nginx,简单…

《SQL基础》11. 索引

SQL - 索引索引概述结构B-TreeBTreeHash思考分类语法SQL性能分析SQL执行频率慢查询日志profile详情explain执行计划索引失效情况范围查询索引列运算字符串不加引号模糊查询or连接条件数据分布影响使用规则最左前缀法则SQL提示覆盖索引前缀索引设计原则索引 概述 索引&#xf…

linux安装minio,注册服务实现开机自启动

一、安装1.创建安装目录mkdir -p /usr/local/soft/minio2.下载文件cd /usr/local/soft/miniowget -q http://dl.minio.org.cn/server/minio/release/linux-amd64/minio3.新建数据存储目录mkdir -p /home/minio/data4.授予可执行权限chmod x minio5.设置账号密码export MINIO_AC…

yolov8代码梳理 训练自己的数据 最终版

1.总结一下 最开始为了检测不规则的麻包袋&#xff0c;所以采用了目标检测。yolov3&#xff0c;fasterrcnn&#xff0c;ssd。这种矩形框还是可以用。后面检测的物体变成了规则的纸箱&#xff0c;我们还用目标检测发现&#xff0c;没有旋转角度&#xff0c;因为箱子的摆放不是正…

深度学习算法面试常问问题(三)

pooling层是如何进行反向传播的&#xff1f; average pooling&#xff1a; 在前向传播中&#xff0c;就是把一个patch的值取平均传递给下一层的一个像素。因此&#xff0c;在反向传播中&#xff0c;就是把某个像素的值平均分成n份 分配给上一层。 max pooling&#xff1a; 在前…

一文教你如何编写高效的接口测试,别再浪费时间了

目录 前言 1、什么是接口测试 2、为什么要做接口测试 3、接口测试的范围 4、接口测试的重点 5、测试原则 总结 前言 在所有的开发测试中&#xff0c;接口测试是必不可少的一项。有效且覆盖完整的接口测试&#xff0c;不仅能保障新功能的开发质量&#xff0c;还能让开发在…

PMP项目管理未来的发展与趋势

什么是项目管理&#xff1f;关于项目管理的解释主要是基于国际项目管理三大体系不同的解释及本领域权威专家的解释。 项目管理就是以项目为对象的系统管理方法&#xff0c;通过一个临时性的、专门的柔性组织&#xff0c;对项目进行高效率的计划、组织、指导和控制&#xff0c;以…

深度学习术语解释:backbone、head、neck,etc

backbone&#xff1a;翻译为主干网络的意思&#xff0c;既然说是主干网络&#xff0c;就代表其是网络的一部分&#xff0c;那么是哪部分呢&#xff1f;这个主干网络大多时候指的是提取特征的网络&#xff0c;其作用就是提取图片中的信息&#xff0c;共后面的网络使用。这些网络…

深入浅出带你学习利用session.upload_progress进行文件包含

前言 该思路是很久之前在CTF比赛中学习到的&#xff0c;可以简单理解为利用session.upload_progress来进行文件竞争从而达到上传文件进行文件包含或者命令执行的目的&#xff0c;可能大部分人会不理解&#xff0c;我们下面我们展开来讲。 基础知识 session.upload_progress …

Java并发编程面试题——线程安全(原子性、可见性、有序性)

文章目录一、原子性高频问题1.1 Java中如何实现线程安全?1.2 CAS底层实现1.3 CAS的常见问题1.4 四种引用类型 ThreadLocal的问题&#xff1f;二、可见性高频问题2.1 Java的内存模型2.2 保证可见性的方式2.3 volatile修饰引用数据类型2.4 有了MESI协议&#xff0c;为啥还有vol…

2022年全国职业院校技能大赛网络安全竞赛A模块(2)

2022年全国职业院校技能大赛&#xff08;中职组&#xff09; 网络安全竞赛试题 &#xff08;2&#xff09; 目录 2022年全国职业院校技能大赛&#xff08;中职组&#xff09; 网络安全竞赛试题 &#xff08;2&#xff09; 模块A 基础设施设置与安全加固 A-1任务一 登录安…

ChatGPT引爆资本狂潮,AI大战升级版打响!谷歌紧急上线对标竞品,微软测试AI搜索引擎,百度宣国产版定档,谁才是最后的赢家?

文 BFT机器人 如果你迄今为止还没有听说过ChatGPT&#xff0c;那就已经彻彻底底被人工智能的新时代甩到身后了。 如果说去年年底ChatGPT刚推出的时候&#xff0c;大多数网友对其的态度还是认为它和Clubhouse一样仅会是昙花一现的话&#xff0c;那么2个月时间过去&#xff0c;C…

【Ubuntu换源教程】不同Ubuntu系统版本换清华源

今天在新电脑上装了虚拟机VMware Workstation Pro 16&#xff0c;在虚拟机上安装了Ubuntu20.04系统。 在做Ubuntu20.04系统换源的时候&#xff0c;发现源要和Ubuntu的版本匹配&#xff0c;之前一直不知道&#xff0c;一直都是盲目换源&#xff0c;版本如果不匹配的话&#xff…