C++项目 -- 高并发内存池(二)Thread Cache

C++项目 – 高并发内存池(二)Thread Cache

文章目录

  • C++项目 -- 高并发内存池(二)Thread Cache
  • 一、高并发内存池整体框架设计
  • 二、thread cache设计
    • 1.整体设计
    • 2.thread cache哈希桶映射规则
    • 3.TLS无锁访问
    • 4.thread cache代码


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

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

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

concurrent memory pool主要由以下3个部分构成:
在这里插入图片描述

  1. thread cache线程缓存每个线程独有的,用于小于256KB的内存的分配,线程从这里申请内存不需要加锁,每个线程独享一个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对象,并且合并相邻的页,组成更大的页,缓解内存碎片的问题。

二、thread cache设计

1.整体设计

thread cache是哈希桶结构每个桶是一个按桶位置映射大小的内存块对象的自由链表。每个线程都会有一个thread cache对象,这样每个线程在这里获取对象和释放对象时是无锁的。

  • 类比定长内存池,一大块内存用来分配空间,freeList用于管理已经分配好的定长内存块
  • thread cache的freeList可以设计多个大小的定长list,如下图,8字节、16字节…;对象小于等于8字节的由8字节的freelist管理,9到16字节的由16字节的freeList管理;
    在这里插入图片描述
  • 但这样设计会造成空间浪费,比如一个对象大小为6字节,为它分配了8字节的空间,那么这剩下的2字节就会成为碎片,这叫做内碎片
  • thread cache整体是一个哈希桶结构,将对象的大小映射到对应大小的freeList中进行管理;

申请内存:

  • 当内存申请size<=256KB,先获取到线程本地存储的thread cache对象,计算size映射的哈希桶自由链表下标i。
  • 如果自由链表_freeLists[i]中有对象,则直接Pop一个内存对象返回。
  • 如果_freeLists[i]中没有对象时,则批量从central cache中获取一定数量的对象,插入到自由链表并返回一个对象。

释放内存:

  • 当释放内存小于256k时将内存释放回thread cache,计算size映射自由链表桶位置i,将对象Push到_freeLists[i]。
  • 当链表的长度过长,则回收一部分内存对象到central cache。

2.thread cache哈希桶映射规则

  • thread cache最大支持一个对象申请256KB的内存空间,则对象的大小范围是1 ~ 256KB,如果以8字节为对齐数,指定freeList,就是1 ~ 8字节大小的对象按照8字节对齐,分配8字节空间,连接到第一个freeList后面;9 ~ 16字节大小的对象分配16字节空间,连接到第二个freeList后面,这就是内存对齐
  • 每一个对齐的freeList都是一个哈希桶,这就是哈希映射
    在这里插入图片描述
  • 如果将所有的哈希桶对齐规则都为8字节,则一共需要32768个哈希桶,数量太多;

因此我们需要制定一个对齐规则:
在这里插入图片描述
分段安排对齐数的大小;

  • 对象大小在1 ~ 128字节之间的,按照8字节对齐,即每个哈希桶大小增长8字节;
  • 那么8字节对齐的哈希桶就有16个,即对应freeLists[0] - freeList[15];
    第一个桶链接对象大小在1 ~ 8字节之间的,第二个链接9 ~ 16字节之间,依次类推;
  • 对象大小在129 - 1024之间的,按照16字节对齐,即每个哈希桶大小增长16字节,以此类推;
  • 最终分配下来,8字节对齐的哈希桶共16个,16字节对齐的哈希桶共56个,128字节对齐的哈希桶共56个,1024字节对齐的哈希桶共56个,8KB字节对齐的哈希桶共56个,所有的哈希桶加起来一共208个,也就是说一共有208个freeList;
  • 根据上面的对齐规则可以将对象的size按照对齐数进行对齐,对齐的逻辑:
    在这里插入图片描述
    如果size不是对齐数alignNum的倍数,就需要根据对齐数调整最终分配空间的大小,否则size就是最终分配空间的大小;
  • 上面的计算过程可以使用下面的位运算进行代替,因为这段代码会被频繁调用,位运算的效率更高:
    在这里插入图片描述

这样分配的好处:

  • 能够减少哈希桶的数量
  • 能够将内碎片浪费控制在10%左右
    • 以16字节对齐为例,16字节对齐数能浪费的空间最大为15字节;如果一个对象分配到了129字节的内存,其对应的对齐数是16,则最终系统会为该对象分配145字节的空间,那么就有15字节的空间浪费,则内碎片为15字节,浪费率 = 15 / 145 = 0.1034 ,后面128字节等的对齐规则类似
    • 前面8字节对齐的部分可能不止10%,但是从16字节开始就能够控制在10%左右;

在制定好字节对齐规则后,还需要制订哈希映射规则,将不同大小的对象映射到对应的freeList中:

  • 根据字节对齐规则,1 ~ 128字节大小的对象,映射在8字节对齐的哈希桶,其映射逻辑如下:
    在这里插入图片描述
    129 ~ 1024字节大小的对象也是这样的规则;
  • 上面的代码可以使用位运算代替,因为这段代码会被频繁调用,位运算的效率更高:
    在这里插入图片描述

3.TLS无锁访问

在多线程环境下,ThreadCache的创建和访问会涉及到锁的问题,我们希望每个线程都有独立的ThreadCache,并且访问自己的ThreadCache都无须加锁,这样就需要使用TLS;
线程局部存储(TLS),是一种变量的存储方法,这个变量在它所在的线程内是全局可访问的,但是不能被其他线程访问到,这样就保持了数据的线程独立性。而熟知的全局变量,是所有线程都可以访问的,这样就不可避免需要锁来控制,增加了控制成本和代码复杂度。

Linux下TLS
win下TLS

TLS —— thread local storage:线程本地存储,我们这里使用静态的TLS:
声明以下代码:

_declspec(thread) DWORD data=0;

声明了_declspec(thread)的变量,会为每一个线程创建一个单独的拷贝。

  • 静态TLS的原理
    在×86CPU上,将为每次引用的静态TLS变量生成3个辅助机器指令。如果在进程中创建了另一个线程,那么系统就要将它捕获并且自动分配另一个内存块,以便存放新线程的静态TLS变量。新线程只拥有对它自己的静态TLS变量的访问权,不能访问属于其他线程的TLS变量。

ThreadCache.h
在这里插入图片描述

  • 在该头文件下定义一个全局的ThreadCache*类型的静态指针pTLSThreadCache使用_declspec(thread)声明后,每一个线程都会为该指针创建一个单独的拷贝,本线程在访问pTLSThreadCache指针时是能够全局访问的,但是其他线程不能访问本线程的pTLSThreadCache指针,这样就实现了多线程环境下的无锁访问;
  • 其实就是每一个线程都有一个独立的pTLSThreadCache,线程之间访问互不干扰

ConcurrentAlloc.h
在这里插入图片描述

  • 上面的代码只是声明了TLS的指针,并没有指向实际的ThreadCache对象,实际上每个线程在运行的时候,都需要将TLS指针指向自己的ThreadCache对象;
  • 这个头文件是对多线程环境下ThreadCache申请和释放内存的功能进行了封装,保证每个线程的ThreadCache都是本线程独有的,
  • ConcurrentAlloc函数会检测pTLSThreadCache是否为空,如果为空,证明初次调用,就需要构建一个新的ThreadCache对象,并将pTLSThreadCache指针指向该对象,这样本线程独有的ThreadCache对象就创建好了,再通过pTLSThreadCache指针去调用Allocate函数开辟空间;

4.thread cache代码

Common.h

  • 公共的头文件,共有部分的代码可以写在这里面;
  • NextObj函数用于获取当前obj对象指向的下一个对象的指针:
  • 将自由链表定义为一个类FreeList,实现链表的基本操作
  • 定义一个管理字节对齐和哈希映射规则的类SizeClass
    • 类中的所有成员函数都定义为静态的内联函数,方便外部直接调用;
    • RoundUp是用来计算当前对象size字节对齐之后对应的size,先判断对象的size在哪个对齐区间,再根据对齐数来计算对齐后的size(调用子函数_RoundUp
    • Index函数用来计算对象size映射到哪一个哈希桶(freelist),根据对象size所属的对齐区间和对齐数,调用_Index函数计算该对象映射到的哈希桶下标;
      注意从第二个对齐区间开始,由于前面部分的对齐数是不同的,因此在计算下标的时候,先要用size减去前面不同对齐数的区间,带入_Index函数计算该对象在当前区间内的相对下标,最后再加上前面所有的哈希桶个数,得到最终的下标;
      例如:如果映射到的是16字节对齐的区域,先要用分配空间减去128,因为前面的128字节是8字节对齐的,要减去,剩下的按照16字节对齐计算,再加上前面减去的桶的数量,以此类推
#pragma once
//公共头文件#include <iostream>
#include <vector>
#include <assert.h>
#include <thread>
using std::cout;
using std::endl;
using std::vector;static const size_t MAX_BYTES = 256 * 1024; //ThreadCache能分配对象的最大字节数
static const size_t NFREELIST = 208; // 最大的哈希桶数量// 访问obj的前4 / 8字节地址空间
static 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;
};// 管理对齐和哈希映射规则的类
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)//RoundUp的子函数,根据对象大小和对齐数,返回对象对齐后的大小static inline size_t _RoundUp(size_t size, size_t align) {//if (size % align == 0) {//	return size;//}//else {//	return (size / align + 1) * align;//}//使用位运算能够得到一样的结果,但是位运算的效率很高return ((size + align - 1) & ~(align - 1));}//计算当前对象size字节对齐之后对应的sizestatic inline size_t RoundUp(size_t size) {assert(size <= MAX_BYTES);if (size <= 128) {//8字节对齐_RoundUp(size, 8);}else if (size <= 1024) {//16字节对齐_RoundUp(size, 16);}else if (size <= 8 * 1024) {//128字节对齐_RoundUp(size, 128);}else if (size <= 64 * 1024) {//1024字节对齐_RoundUp(size, 1024);}else if (size <= 256 * 1024) {//8KB字节对齐_RoundUp(size, 8 * 1024);}else {assert(false);}return -1;}//Index的子函数,用于计算映射的哈希桶下标static inline size_t _Index(size_t size, size_t alignShift) {//if (size % align == 0) {//	return size / align - 1;//}//else {//	return size / align;//}//使用位运算能够得到一样的结果,但是位运算的效率很高//使用位运算需要将输入参数由对齐数改为对齐数是2的几次幂、return ((size + (1 << alignShift) - 1) >> alignShift) - 1;}//计算对象size映射到哪一个哈希桶(freelist)static inline size_t Index(size_t size) {assert(size <= MAX_BYTES);//每个区间有多少个哈希桶static int groupArray[4] = { 16, 56, 56, 56 };if (size <= 128) {return _Index(size, 3);}else if (size <= 1024) {//由于前128字节不是16字节对齐,因此需要减去该部分,单独计算16字节对齐的下标//再在最终结果加上全部的8字节对齐哈希桶个数return _Index(size - 128, 4) + groupArray[0];}else if (size <= 8 * 1024) {return _Index(size - 1024, 7) + groupArray[0] + groupArray[1];}else if (size <= 64 * 1024) {return _Index(size - 8 * 1024, 10) + groupArray[0] + groupArray[1] + groupArray[2];}else if (size <= 256 * 1024) {return _Index(size - 64 * 1024, 13) + groupArray[0] + groupArray[1] + groupArray[2] + groupArray[3];}else {assert(false);}return -1;}
};

ThreadCache.h

  • 用于声明ThreadCache类的头文件
  • ThreadCache类包括一个FreeList类型的数组,这就是哈希桶的数组,还有完成ThreadCache功能的成员函数的声明;
  • 定义了由_declspec(thread)声明的TLS指针,用于实现无锁访问
#pragma once
#include "Common.h"class ThreadCache {
public://申请和释放对象内存void* Allocate(size_t size);void Deallocate(void* obj, size_t size);//从中心缓存获取对象void* FetchFromCentralCache(size_t index, size_t alignSize);private:FreeList _freeLists[];
};//声明_declspec(thread)后,会为每一个线程创建一个单独的拷贝
//使用_declspec(thread)声明了ThreadCache*指针变量,则该指针在该线程中会创建一份单独的拷贝
//pTLSThreadCache指向的对象在本线程内是能够全局访问的,但是无法被其他线程访问到,这就做到了多线程情景下的无锁访问
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;

ThreadCache.cpp

  • 这个.cpp文件中来实现ThreadCache中的成员函数:
  • Allocate函数为对象申请内存空间
    • 先获取对齐后的size和对应的哈希桶下标
    • 如果该哈希桶的freeList不为空,就Pop一个内存块给该对象,如果为空就需要向CentralCache申请空间
  • Deallocate函数用于归还对象的内存空间
    • 先获取对象对应的freeList的下标
    • 直接将该内存块插入对应的freeList中
  • FetchFromCentralCache用于从中心缓存获取对象空间
#include "ThreadCache.h"void* ThreadCache::Allocate(size_t size) {assert(size <= MAX_BYTES);//获取对齐后的大小及对应的哈希桶下标size_t alignSize = SizeClass::RoundUp(size);size_t index = SizeClass::Index(size);if (!_freeLists[index].Empty()) {//若对应的freeList桶不为空,直接pop一个内存块给该对象return _freeLists[index].Pop();}else {//否则需要从CentralCache获取内存空间return ThreadCache::FetchFromCentralCache(index, alignSize);}
}void ThreadCache::Deallocate(void* obj, size_t size) {assert(obj);assert(size <= MAX_BYTES);//找该对象对应的freeList的桶,直接插入size_t index = SizeClass::Index(size);_freeLists[index].Push(obj);
}void* ThreadCache::FetchFromCentralCache(size_t index, size_t alignSize) {return nullptr;
}

ConcurrentAlloc.h

  • 该头文件用于进一步封装ThreadCache的功能,进而使其能够实现多线程情况下的无锁访问
#pragma once
#include "Common.h"
#include "ThreadCache.h"static void* ConcurrentAlloc(size_t size) {if (pTLSThreadCache == nullptr) {//如果pTLSThreadCache指针是空的,就构造一个ThreadCache对象,并指向它//则这个ThreadCache对象就是本线程专属的ThreadCache对象pTLSThreadCache = new ThreadCache;}//使用pTLSThreadCache访问本线程专属的ThreadCache对象来开辟空间return pTLSThreadCache->Allocate(size);
}static void ConcurrentFree(void* obj, size_t size) {assert(pTLSThreadCache);pTLSThreadCache->Deallocate(obj, size);
}

UnitTest.cpp

  • 该cpp文件用于测试ThreadCache的功能,重点测试TLS无锁访问的功能
  • 创建两个线程,分别使用ThreadCache申请空间,使用并行监视窗口监视每个进程的内容;
  • c++11的多线程,是将一个线程封装成一个对象
    在这里插入图片描述
    构造thread对象时,传入该线程执行的函数的指针以及参数;
#include "ObjectPool.h"
#include "ConcurrentAlloc.h"
#include "ThreadCache.h"void Alloc1() {for (int i = 0; i < 5; i++) {void* obj = ConcurrentAlloc(5);}
}void Alloc2() {for (int i = 0; i < 5; i++) {void* obj = ConcurrentAlloc(8);}
}void TestTLS() {std::thread t1(Alloc1);std::thread t2(Alloc2);t1.join();t2.join();
}int main() {TestTLS();return 0;
}

测试结果:
在这里插入图片描述

  • 两个不同的线程都获取和ThreadCache对象,但是通过TLS获取到的是两个不同的ThreadCache,每个线程各一个,两个线程通过pTLSThreadCache指针访问各自的ThreadCache对象

  • 也可以通过输出线程id和pTLSThreadCache指针来观察验证
    在这里插入图片描述
    在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Log360,引入全新安全与风险管理功能,助力企业积极抵御网络威胁

ManageEngine在其SIEM解决方案中推出了安全与风险管理新功能&#xff0c;企业现在能够更主动地减轻内部攻击和防范入侵。 SIEM 这项新功能为Log360引入了安全与风险管理仪表板&#xff0c;Log360是ManageEngine的统一安全信息与事件管理&#xff08;SIEM&#xff09;解决方案…

51单片机之LED灯模块篇

御风以翔 破浪以飏 &#x1f3a5;个人主页 &#x1f525;个人专栏 目录 点亮一盏LED灯 LED的组成原理 LED的硬件模型 点亮一盏LED灯的程序设计 LED灯闪烁 LED流水灯 独立按键控制LED灯亮灭 独立按键的组成原理 独立按键的硬件模型 独立按键控制LED灯状态 按键的抖动 独立按键…

Unity中blendtree和state间的过渡

混合树状态之间的过渡 如果属于此过渡的当前状态或下一状态是混合树状态&#xff0c;则混合树参数将出现在 Inspector 中。通过调整这些值可预览在混合树值设置为不同配置时的过渡表现情况。 如果混合树包含不同长度的剪辑&#xff0c;您应该测试在显示短剪辑和长剪辑时的过渡表…

ubuntu22.04 经常死机,鼠标,键盘无响应

一、现象说明 1. 开机一小时后&#xff0c;突然之间网络掉线&#xff0c;鼠标、键盘无反应。 2.强制重启后&#xff0c;恢复正常。 3.多次重复出现该问题。 二、环境说明&#xff1a;内核、显卡 三、异常日志&#xff1a; /var/log/syslog: 四、问题解答&#xff1a; 1.…

ChatGPT Plus如何升级?信用卡付款失败怎么办?如何使用信用卡升级 ChatGPT Plus?

ChatGPT Plus是OpenAI提供的一种高级服务&#xff0c;它相较于标准版本&#xff0c;提供了更快的响应速度、更强大的功能&#xff0c;并且用户可以优先体验到新推出的功能。 尽管许多用户愿意支付 20 美元的月费来订阅 GPT-4&#xff0c;但在实际支付过程中&#xff0c;特别是…

【项目实践03】【布隆过滤器】

文章目录 一、前言二、项目背景三、实现方案1. 谷歌 布隆过滤器2. Redis 布隆过滤器 四、思路延伸1. 布隆过滤器的实现原理2. 布隆过滤器的一些扩展3. 布谷鸟过滤器 五、参考内容 一、前言 本系列用来记录一些在实际项目中的小东西&#xff0c;并记录在过程中想到一些小东西&a…

神经网络 | 基于 CNN 模型实现土壤湿度预测

Hi&#xff0c;大家好&#xff0c;我是半亩花海。在现代农业和环境监测中&#xff0c;了解土壤湿度的变化对于作物生长和水资源管理至关重要。通过深度学习技术&#xff0c;特别是卷积神经网络&#xff0c;我们可以利用过去的土壤湿度数据来预测未来的湿度趋势。本文将使用 Pad…

基于深度学习的SSVEP分类算法简介

基于深度学习的SSVEP分类算法简介 1、目标与范畴2、深度学习的算法介绍3、参考文献 1、目标与范畴 稳态视觉诱发电位&#xff08;SSVEP&#xff09;是指当受试者持续注视固定频率的闪光或翻转刺激时&#xff0c;在大脑枕-额叶区域诱发的与刺激频率相关的电生理信号。与P300、运…

从领域外到领域内:LLM在Text-to-SQL任务中的演进之路

导语 本文介绍了ODIS框架&#xff0c;这是一种新颖的Text-to-SQL方法&#xff0c;它结合了领域外示例和合成生成的领域内示例&#xff0c;以提升大型语言模型在In-context Learning中的性能。 标题&#xff1a;Selective Demonstrations for Cross-domain Text-to-SQL会议&am…

计算机网络第6章(应用层)

6.1、应用层概述 我们在浏览器的地址中输入某个网站的域名后&#xff0c;就可以访问该网站的内容&#xff0c;这个就是万维网WWW应用&#xff0c;其相关的应用层协议为超文本传送协议HTTP 用户在浏览器地址栏中输入的是“见名知意”的域名&#xff0c;而TCP/IP的网际层使用IP地…

ubuntu离线安装k8s

目录 一、前期准备 二、安装前配置 三、安装docker 四、安装cri-dockerd 五、部署k8s master节点 六、整合kubectl与cri-dockerd 七、网络等插件安装 八、常见问题及解决方法 一、前期准备 ①ubuntu系统 本地已安装ubuntu系统&#xff0c;lsb_release -a命令查看版本信…

论文阅读-CARD:一种针对复制元数据服务器集群的拥塞感知请求调度方案

论文名称&#xff1a;CARD: A Congestion-Aware Request Dispatching Scheme for Replicated Metadata Server Cluster 摘要 复制元数据服务器集群&#xff08;RMSC&#xff09;在分布式文件系统中非常高效&#xff0c;同时面对数据驱动的场景&#xff08;例如&#xff0c;大…