【项目日记(四)】thread cache 层

news2025/2/11 8:52:46

前言

前面我们对整个项目的框架进行了介绍,本期开始我们将进行第一层线程缓存层(thread cache)的详细介绍与实现。

目录

前言

一、thread cache 的整体设计

二、内存对齐规则和哈希映射关系

2.1 如何对齐?

2.2 这样设计对齐规则的好处?

2.3 内存对齐和哈希桶映射实现

三、TLS 无锁的访问


一、thread cache 的整体设计

thread cache 实际上是一个 哈希桶 的结构,每个桶都是按照桶位置进行映射的管理固定内存块的自由链表。每个线程都会有一个 thread cache  对象,因此线程 申请 和 释放 内存时无锁的!

因为每个线程最大可以申请 256 KB 的大小,也有可能申请 1bytes 的大小,如果我们都按一个字节进行划分内存对象话,那光管理这些内存块的自由链表就得 20 多万个,仅仅是存储自由链表的指针都是一个很大的开销,所以不能按字节划分!

我们可选择多一些平衡的牺牲,让这些字节数按照某种对齐规则进行向上对齐。例如:我们让这些都按照 8 字节对齐,那么申请[1-8]字节,都统一给8字节,申请[9-16]字节就给16字节。即thread cache 的结构就是下面这样的:

当前自由链表中挂的一些内存对象是经过 内存对齐 的也就是实际的空间是 大于等于 申请 的空间的,此时多余的那部分无法被利用。多余的那部分就成为内碎片

例如,按照 8 字节对齐,申请 1 字节,但是实际有 8 字节,多出的那7字节,就是内碎片。

也就是说,外碎片是空间总量够,但是开不出大块内存。内碎片是由于对齐而无法使用的那部分空间。


由于整个项目比较复杂所以我们先对自由链表进行封装,而当前的自由链表后面也会用到,所以我们放在 common.h

我们目前只有三个接口, 向自由链表添加内存对象 Push 从自由链表中 拿出一个 Pop判断自由链表是否为空 Empty 。后期的接口我们到时候再加。

// 获取 obj 对象的 头部的 4/8 字节
static inline void*& NextObj(void* obj)
{
	return *(void**)obj;
}

class FreeList
{
public:
	void Push(void* obj)
	{
		assert(obj);
		// 头插
		NextObj(obj) = _freelist;
		_freelist = obj;
	}

	void* Pop()
	{
		assert(_freelist);
		// 头删
		void* obj = _freelist;
		_freelist = NextObj(obj);
		return obj;
	}

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

private:
	void* _freelist = nullptr;
};

有了自由链表的封装和上面的介绍,我们就可以先将 thread cache 的整体框架搭建出来:

它里面的成员属性就一个 自由链表的数组(哈希桶)。至于这个数组有多大,得根据具体的对齐规则确定,暂时这用 N 代替

成员方法有三个:1、申请内存  2、释放内存 3、去 central cache 申请

class ThreadCache
{
public:
	// 申请和释放内存对象
	void* Allocate(size_t size);// size 代表 申请的大小 字节数
	void Deallocate(void* ptr, size_t size);// ptr 代表还回来内存的地址,size 表示大小

	// 从中心缓存获取对象
	void* FetchFromCentralCache(size_t index, size_t size); // index 表示 哈希桶的下标,size 表示申请的内存大小

private:
	FreeList _freelists[N];// 哈希桶
};

当线程申请某一大小的内存时,会调用 Allocate 它内部根据某种对齐规则计算对齐后的大小,然后更具哈希桶的映射规则找到对应的自由链表,如果自由链表不为空头删,返回一个内存对象即可,否则,调用FetchFromCentralCachecentral cache 中申请,然后返回。同理,释放的内存对象调用 Deallocate,它内部根据内存大小进行映射找到哈希桶中的对应的自由链表,然后进行头插即可!

我们本期主要实现申请和释放,从中心缓存申请我们要结合下一层实现,这个在下一层在实现!

而要实现这个两个接口,我们就得线确定是什么内存对齐规则了!


二、内存对齐规则和哈希映射关系

2.1 如何对齐?

上面说了,不能每个字节都对应一个自由链表,否则花销太大。因此我们需要指定一种合适的对齐规则。

首先,我们得保证每个内存块是可以链接到自由链表上的,因此,一开始 给 8 最合适,这样无论 32位 还是 64 位下都可以存的下一个指针。

在者我们也不能都按照 8 字节对齐,因为如果都按照 8 字节对齐,的话 我们需要建立 3万多个桶(256 * 1024 / 8 = 32768)这样也不行,其实我们可以分段,每一段按照不同的对其数进行对齐,具体如下:

2.2 这样设计对齐规则的好处?

这样设计,内碎片的浪费率可以控制在 10% 左右(浪费率 = 浪费的字节数 / 对齐后的字节数)!

首先第一组就不讨论了,因为你如果申请 1 字节,就算对齐数 是 2  浪费率也是 50 % 我们从第二组开始!

根据上面的公式,我们要得到某个区间的最大浪费率,就应该让分子取到最大,让分母取到最小。比如129~1024这个区间,该区域的对齐数是16,那么最大浪费的字节数就是15,而最小对齐后的字节数就是这个区间内的前16个数所对齐到的字节数,也就是144,那么该区间的最大浪费率也就是15 ÷ 144 ≈ 10.42 %  同样的道理,后面两个区间的最大浪费率分别是127 ÷ 1152 ≈ 11.02 % 和1023 ÷ 9216 ≈ 11.10 %

2.3 内存对齐和哈希桶映射实现

有了上述的对齐规则,我们需要实现两个函数,分别获取某一字节数进行对齐后的字节数,和该字节数对应哈希桶的下标,我们可以将他封装在一个类当中:

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

因为这个类中没有成员,我们可以设为静态的否则调用时还要创建对象,另外我们这些函数会频繁的调用所以设置为内联


当我们我去到申请的内存根据其字节时,可以判断它属于哪一个区间,然后将申请的大小和对其数给子函数让子函数去进行对齐工作:

// 根据 bytes 和 alignNUm 进行对齐
static inline size_t _RoundUp(size_t bytes, size_t alignNum)
{

}

// 获取向上对齐后的字节数
static inline size_t RoundUp(size_t bytes)
{

	// 整体控制在最多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)
	assert(bytes <= MAX_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
	{
		// 理论上不会走到这里,避免程序出错到这里,我们直接断言结束
		assert(false);
		return -1;
	}
}

下面追要工作就是实现这个子函数了!一般我们可能想到的做法就是:

static inline size_t _RoundUp(size_t bytes, size_t alignNum)
{
	size_t alignSize = 0;
	if (bytes % alignNum == 0)
	{
		// 申请的字节数是对其数的整数倍,无需对其
		alignSize = bytes;
	}
	else
	{
		// 因为向上取整,所以让 bytes / alingNum 的商 + 1就想上取整了,然后 在 乘 alignNum 即可
		alignSize = (bytes / alignNum + 1) * alignNum;
	}

	return alignSize;
}

这也是最能想到的方法,但是除了这种方法之外,还有一种位运算的方法很精妙:

static inline size_t _RoundUp(size_t bytes, size_t alignNum)
{
	// 当前字节数 + 对其数 - 1 就永远到不了,对其数的下一个整数倍,但是 >= alignNum 的 
	// 当前的结果 是由 最大对其数的 位数 中的 其中几位 组合而成的
	// 再将 对其数 -1 取反,意味着最大对其数的 最高位不变,后面的低位全部取反
	// 在和 + 对其数 - 1 的结果 按位与,就只剩下 对其数了
	// 例如:[1-8] --> + 8-1 = 7 ===> [8-15], 7 取反 == 1000 & [8-15] == 8
	return (bytes + (alignNum - 1)) & ~(alignNum - 1);
}

在获取某一字节对应的下标时,我么也可以根据字节的大小判断他在哪一个区间,然后调用子函数进行处理:

// 获取对应哈希桶的下标
static inline size_t Index(size_t bytes)
{
	assert(bytes <= MAX_BYTES);

	// 每个区间有多少个 桶(链)
	static int grounp_array[4] = { 16, 56, 56, 56 };

	if (bytes <= 128)
	{
		return _Index(bytes, 3); // 2^3
	}
	else if (bytes <= 1024)
	{
		// 这里因为每个区间采用的对齐数不一样,说我我们 采用绝对映射
		// 最后为了映射的正确性,我们需要加上 前面的一段的 桶的个数
		return _Index(bytes - 128, 4) + grounp_array[0];// 2^4
	}
	else if (bytes <= 8 * 1024)
	{
		return _Index(bytes - 1024, 7) + grounp_array[0] + grounp_array[1];// 2^7
	}
	else if (bytes <= 64 * 1024)
	{
		return _Index(bytes - 8 * 1024, 10) + grounp_array[0] + grounp_array[1] + grounp_array[2];// 2^10
	}
	else if (bytes <= 256 * 1024)
	{
		return _Index(bytes - 64 * 1024, 13) + grounp_array[0] + grounp_array[1] + grounp_array[2] + grounp_array[3];// 2^13
	}
	else
	{
		// 为了避免程序出错走到这里
		assert(false);
		return -1;
	}
}

子函数的实现,也有两种方式,第一种是我觉的我们可以想到的,就是使用 字节大小和对其数 做除法,根据是否整除判定:

// 根据字节数和对其数判断 映射的位置
static inline size_t _Index(size_t bytes, size_t alignNum)
{
	if (bytes % alignNum == 0)
	{
		// 如果 bytes / alignNum 是整除的,说明当前字节的位置就是, 商 的 前一个
		// 例如:Bytes = 8 ,alignNum = 8 ==> bytes / alignNum == 1, 而 [1-8] 是 0 号桶
		return bytes / alignNum - 1;
	}
	else
	{
		// 否则,上就是 合适的位置
		// 1 / 8 == 0 
		return bytes / alignNum;
	}
}

除了这种方式之外也还有一种很精妙的位运算的方法:他是使用字节数和对齐数的倍数进行操作的:

// 根据字节数和对其数的二进制次方判断 映射的位置
static inline size_t _Index(size_t bytes, size_t align_shift)// align_shift 表好似对其数对应的次方数
{
	// 当前的 bytes + 1 << align_shift) - 1 本质就是 bytes + 对其数 - 1,也就是刚好永远到不了,下一个对其数的倍数处
	// 他因为对其数 是 2^x,所以再将 上述的结果 >> align_shift 就是,对应桶的下一个位置 ,在 -1 即可
	return ((bytes + (1 << align_shift) - 1)) >> align_shift - 1;
}

OK,了解了内存对齐规则后,我们就可以知道 在thread cache 中有 208 个桶

static const size_t MAX_BYTES = 256 * 1024; // thread 最大申请的内存大小为 256 KB
static const size_t N_FREE_LIST = 208; // thread cahce 中一共有 208 个桶

thread cache 申请和释放的逻辑也就很简单了:

// 申请和释放内存对象
void* ThreadCache::Allocate(size_t size)// size 代表 申请的大小 字节数
{
	assert(size <= MAX_BYTES);
	size_t alignSize = SizeClass::RoundUp(size);// 计算对齐数
	size_t index = SizeClass::Index(size);// 计算对应的哈希桶
	// 如果哈希桶中的自由链表不为空,即 有申请的 内存,直接弹出即可
	if (!_freelists[index].Empty())
	{
		return _freelists[index].Pop();
	}
	else
	{
		// 当前映射的哈希桶的自由链表没有内存了,去下一层申请
		return FetchFromCentralCache(index, alignSize);
	}
}

void  ThreadCache::Deallocate(void* ptr, size_t size)// ptr 代表还回来内存的地址,size 表示大小
{
	assert(ptr);
	assert(size <= MAX_BYTES);
	// 根据内存的大小,计算映射下标,插入到自由链表即可
	size_t index = SizeClass::Index(size);
	_freelists[index].Push(ptr);
}

三、TLS 无锁的访问

每一个线程都有一个自己的 thread cache ,但是如何创建他呢?首先不能将创建thread cache 方成全局的,因为全局是所有线程共享的,这样的话就又得使用锁来控制

要实现每个线程无锁的访问属于自己的thread cache,我们需要用到线程局部存储TLS(Thread Local Storage),这是一种变量的存储方法,使用该存储方法的变量在它所在的线程是全局可访问的,但是不能被其他线程访问到,这样就保持了数据的线程独立性。

//TLS - Thread Local Storage
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;

我们可以在提供两个接口,用于 TLS无锁访问的控制:

static void* ConcurrentAlloc(size_t size)
{
	// TLS 无锁访问
	// 当线程调用 alloc 时,才创建 thread cache 对象
	if (pTLSThreadCache == nullptr)
	{
		pTLSThreadCache = new ThreadCache;
	}

	std::cout << std::this_thread::get_id() << ": " << pTLSThreadCache << std::endl;

	return pTLSThreadCache->Allocate(size);
}

static void ConcurrentFree(void* ptr, size_t size)
{
	assert(pTLSThreadCache);
	pTLSThreadCache->Deallocate(ptr, size);
}

简单测试一下,无锁访问:

void Alloc1()
{
	for (size_t i = 0; i < 5; ++i)
	{
		void* ptr = ConcurrentAlloc(6);
	}
}

void Alloc2()
{
	for (size_t i = 0; i < 5; ++i)
	{
		void* ptr = ConcurrentAlloc(7);
	}
}

void testTLS()
{
	std::thread t1(Alloc1);
	std::thread t2(Alloc2);

	t1.join();
	t2.join();
}

int main()
{
	//TestObjectPool();
	testTLS();
	return 0;
}

上面代码的意思是,两个线程分别执行 Alloc1 和 Alloc2  ,如果按照上面的 TLS 的机制,应该是连个线程的 pTLSThreadCache 的值分别都是唯一的:

OK,证明我们当前的逻辑是正确的!这就是 thread cache 层了,下一期我们来探索 central cache 层! 

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

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

相关文章

人工智能图像分割之Mask2former源码解读

环境搭建: (1)首先本代码是下载的mmdetection-2022.9的,所以它的版本要配置好,本源码配置例如mmcv1.7,python3.7,pytorch1.13,cuda11.7。pytorch与python,cuda版本匹配可参考&#xff1a;https://www.jb51.net/python/3308342lx.htm。 (2)还有一个是先要安装一个vs2022版本或…

uniapp 编译生成鸿蒙正式app步骤

1&#xff0c;在最新版本DevEco-Studio工具新建一个空项目并生成p12和csr文件&#xff08;构建-生成私钥和证书请求文件&#xff09; 2&#xff0c;华为开发者平台 根据上面生成的csr文件新增cer和p7b文件&#xff0c;分发布和测试 3&#xff0c;在最新版本DevEco-Studio工具 文…

2024最新版Java面试题及答案,【来自于各大厂】

发现网上很多Java面试题都没有答案&#xff0c;所以花了很长时间搜集整理出来了这套Java面试题大全~ 篇幅限制就只能给大家展示小册部分内容了&#xff0c;需要完整版的及Java面试宝典小伙伴点赞转发&#xff0c;关注我后在【翻到最下方&#xff0c;文尾点击名片】即可免费获取…

Excel 融合 deepseek

效果展示 代码实现 Function QhBaiDuYunAIReq(question, _Optional Authorization "Bearer ", _Optional Qhurl "https://qianfan.baidubce.com/v2/chat/completions")Dim XMLHTTP As ObjectDim url As Stringurl Qhurl 这里替换为你实际的URLDim postD…

21.2.6 字体和边框

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的。 通过设置Rang.Font对象的几个成员就可以修改字体&#xff0c;设置Range.Borders就可以修改边框样式。 【例 21.6】【项目&#xff…

OpenFeign远程调用返回的是List<T>类型的数据

在使用 OpenFeign 进行远程调用时&#xff0c;如果接口返回的是 List 类型的数据&#xff0c;可以通过以下方式处理&#xff1a; 直接定义返回类型为List Feign 默认支持 JSON 序列化/反序列化&#xff0c;如果服务端返回的是 List的JSON格式数据&#xff0c;可以直接在 Feig…

三维模拟-机械臂自翻车

机械仿真 前言效果图后续 前言 最近在研究Unity机械仿真&#xff0c;用Unity实现其运动学仿真展示的功能&#xff0c;发现一个好用的插件“MGS-Machinery-master”&#xff0c;完美的解决了Unity关节定义缺少液压缸伸缩关节功能&#xff0c;内置了多个场景&#xff0c;讲真的&…

网络安全治理架构图 网络安全管理架构

网站安全攻防战 XSS攻击 防御手段&#xff1a; - 消毒。 因为恶意脚本中有一些特殊字符&#xff0c;可以通过转义的方式来进行防范 - HttpOnly 对cookie添加httpOnly属性则脚本不能修改cookie。就能防止恶意脚本篡改cookie 注入攻击 SQL注入攻击需要攻击者对数据库结构有所…

调用deepseek的API接口使用,对话,json化,产品化

背景 最近没咋用chatgpt了&#xff0c;deepseek-r1推理模型写代码质量是很高。deepseek其输出内容的质量和效果在国产的模型里面来说确实算是最强的&#xff0c;并且成本低&#xff0c;它的API接口生态也做的非常好&#xff0c;和OpenAI完美兼容。所以我们这一期来学一下怎么调…

DeepSeek大模型本地部署实战

1. 下载并安装Ollama 打开浏览器&#xff1a;使用你常用的浏览器&#xff08;如Chrome、Firefox等&#xff09;访问Ollama的官方网站。无需特殊网络环境&#xff0c;直接搜索“Ollama”即可找到。 登录与下载&#xff1a;进入Ollama官网后&#xff0c;点击右上角的“Download…

Spring Boot Actuator使用

说明&#xff1a;本文介绍Spring Boot Actuator的使用&#xff0c;关于Spring Boot Actuator介绍&#xff0c;下面这篇博客写得很好&#xff0c;珠玉在前&#xff0c;我就不多介绍了。 Spring Boot Actuator 简单使用 项目里引入下面这个依赖 <!--Spring Boot Actuator依…

[css] 黑白主题切换

link动态引入 类名切换 css滤镜 var 类名切换 v-bind css预处理器mixin类名切换 【前端知识分享】CSS主题切换方案

阿里云专有云网络架构学习

阿里云专有云网络架构 叶脊&#xff08;spine-leaf&#xff09;网络和传统三层网络拓扑对比 阿里云网络架构V3拓扑角色介绍推荐设备设备组网举例带外管理网络带外网和带内网对比设备介绍 安全网络设备介绍 参考 后续更新流量分析叶脊&#xff08;spine-leaf&#xff09;网络和传…

【AIGC】冷启动数据与多阶段训练在 DeepSeek 中的作用

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: AIGC | ChatGPT 文章目录 &#x1f4af;前言&#x1f4af;冷启动数据的作用冷启动数据设计 &#x1f4af;多阶段训练的作用阶段 1&#xff1a;冷启动微调阶段 2&#xff1a;推理导向强化学习&#xff08;RL&#xff0…

GenAI + 电商:从单张图片生成可动态模拟的3D服装

在当今数字化时代,电子商务和虚拟现实技术的结合正在改变人们的购物体验。特别是在服装行业,消费者越来越期待能够通过虚拟试衣来预览衣服的效果,而无需实际穿戴。Dress-1-to-3 技术框架正是为此而生,它利用生成式AI模型(GenAI)和物理模拟技术,将一张普通的穿衣照片转化…

harmonyOS生命周期详述

harmonyOS的生命周期分为app(应用)的生命周期和页面的生命周期函数两部分 应用的生命周期-app应用 在app.js中写逻辑,具体有哪些生命周期函数呢,请看下图: onCreated()、onShow()、onHide()、onDestroy()这五部分 页面及组件生命周期 着重说下onShow和onHide,分别代表是不是…

记一次调整磁盘分区大小的经验

背景 redhat 6 系统 根目录挂载的逻辑卷满了&#xff0c;系统都不能正常运行了 但是/home目录挂载的另外一个逻辑卷却占用只有4% 所以想把/home挂的逻辑卷分一部分给/ 挂的逻辑卷 备份 先把系统整盘备份一下&#xff0c;用clonezilla做一个磁盘镜像&#xff0c;免得失误了搞…

软件测试就业

文章目录 2.6 初识一、软件测试理论二、软件的生产过程三、软件测试概述四、软件测试目的五、软件开发与软件测试的区别&#xff1f;六、学习内容 2.7 理解一、软件测试的定义二、软件测试的生命周期三、软件测试的原则四、软件测试分类五、软件的开发与测试模型1.软件开发模型…

后缀表达式(蓝桥杯19I)

有减于号时 假设有n个大于0从大到小的数&#xff0c;加减符号数为n-1&#xff1a;a,b,c,d,。。。。。&#xff0c;e sum求最大&#xff1a;(max )-(min ) a - (e - ( ) -&#xff08;&#xff09;)( ( )( ) ( ) 。。。。 ) 当序列中有负数时&#xff1a; a -&am…

mac环境下,ollama+deepseek+cherry studio+chatbox本地部署

春节期间&#xff0c;deepseek迅速火爆全网&#xff0c;然后回来上班&#xff0c;我就浅浅的学习一下&#xff0c;然后这里总结一下&#xff0c;我学习中&#xff0c;总结的一些知识点吧&#xff0c;分享给大家。具体的深度安装部署&#xff0c;这里不做赘述&#xff0c;因为网…