C++项目——高并发内存池

一、什么是内存池

内存池(Memory Pool) 是一种动态内存分配与管理技术。

通常情况下,程序员习惯直接使用new、delete、malloc、free 等API申请分配和释放内存,这样导致的后果是:当程序长时间运行时,由于所申请内存块的大小不定,频繁使用时会造成大量的内存碎片从而降低程序和操作系统的性能。

内存池则是在真正使用内存之前,先申请分配一大块内存(内存池)留作备用,当程序员申请内存时,从池中取出一块动态分配,当程序员释放内存时,将释放的内存再放入池内,再次申请池可以 再取出来使用,并尽量与周边的空闲内存块合并。若内存池不够时,则自动扩大内存池,从操作系统中申请更大的内存池。

由于现在硬件条件已经很成熟,大多数运行环境都是多核的,为了提高效率,则高并发这一情况应运而生,对于高并发内存池,则是基于多线程并发申请使用的一个内存池称为高并发内存池。

二、项目介绍

本项目参考了谷歌 tcmalloc 设计模式,设计实现了高并发的内存池。

基于 Windows10 环境 VS2022,采用 C++进行编程,池化技术、多线程、TLS、单例模式、互斥锁、链表、哈希等数据结构。

该项目利用了 thread cache、central、cache、page cache 三级缓存结构,基于多线程申请释放内存的场景,最大程度提高了效率,解决了绝大部分内存碎片问题。

三、项目实现

3.1 设计目标

现代很多的开发环境都是多核多线程,在申请内存的场景下,必然存在激烈的锁竞争问题。所以这次我们实现的内存池需要考虑以下几方面的问题。

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

3.2 项目框架

concurrent memory pool主要由线程缓存(threadcache)、中心缓存(centralcache)、页缓存(pagecache)3个部分构成,如下图

thread cache

为了保证效率,我们使用线程局部存储(thread local storage,TLS)技术保存每个线程本地的ThreadCache的指针,这样大部分情况下申请释放内存是不需要锁的,线程缓存是每个线程独有的,用于小于256k的内存的分配。线程从这里申请内存不需要加锁,每个线程独享一个cache,这也就是这个并发线程池高效的地方

class ThreadCache
{
public:// 申请和释放内存对象void* Allocate(size_t size);void Deallocate(void* ptr, size_t size);// 从中心缓存获取对象void* FetchFromCentralCache(size_t index, size_t size);// 释放对象时,链表过⻓时,回收内存回到中⼼缓存void ListTooLong(FreeList& list, size_t size);
private:FreeList _freeLists[NFREELIST];
};// TLS thread local storage
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;

该结构每个位置存放一个FreeList,每个自由链表下都可以挂自己的内存块。

申请内存:

  1. 当内存申请size<=64k时在thread cache中申请内存,计算size在自由链表中的位置,如果自由链表中有内存对象时,直接从FistList[i]中Pop一下对象,时间复杂度是O(1),且没有锁竞争。
  2. 当FreeList[i]中没有对象时,则批量从central cache中获取一定数量的对象,插入到自由链表并返回一个对象。

释放内存:

  1. 当释放内存小于64k时将内存释放回thread cache,计算size在自由链表中的位置,将对象Push到FreeList[i].
  2. 当链表的长度过长,则回收一部分内存对象到central cache。

central cache

中心缓存是所有线程共享本质是由一个哈希映射的span对象自由链表构成thread cache是按需从central cache中获取的对象

central cache周期性的回收thread cache中的对象,避免一个线程占用了太多的内存,而其他线程的内存吃紧。达到内存分配在多个线程中更均衡的按需调度的目的。

central cache存在竞争的,所以从这里取内存对象是需要加锁,不过一般情况下在这里取内存对象的效率非常高,所以这里竞争不会很激烈。

这里要注意,要保证centralcache是全局唯一的,这里我们需要将centralcache类设计成单例模式

// 单例模式——饿汉模式
class CentralCache
{
public:static CentralCache* GetInstance(){return &_sInst;}// 获取⼀个非空的spanSpan* GetOneSpan(SpanList& list, size_t size);// 从中⼼缓存获取⼀定数量的对象给thread cachesize_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size);// 将⼀定数量的对象释放到span跨度void ReleaseListToSpans(void* start, size_t size);
private:SpanList _spanLists[NFREELIST];private:CentralCache(){}CentralCache(const CentralCache&) = delete;static CentralCache _sInst;
};

申请内存:

  1. 当thread cache中没有内存时,就会批量向central cache申请一些内存对象,central cache也有一个哈希映射的freelist,freelist中挂着span,从span中取出对象给thread cache,这个过程是需要加锁的。
  2. central cache中没有非空的span时,则将空的span链在一起,向page cache申请一个span对象,span对象中是一些以页为单位的内存,切成需要的内存大小,并链接起来,挂到span中。
  3. central cache的span中有一个use_count,分配一个对象给thread cache,就use_count++。

释放内存:

  1. 当thread_cache过长或者线程销毁,则会将内存释放回central cache中的,释放回来时use_count--。当use_count减到0时则表示所有对象都回到了span,则将span释放回pagecache,page cache中会对前后相邻的空闲页进行合并。

page cache

页缓存是在central cache缓存上面的一层缓存,存储的内存是以页为单位存储及分配的,central cache没有内存对象时,从page cache分配出一定数量的page,并切割成定长大小的小块内存,分配给central cache。

page cache会回收central cache满足条件的span对象,并且合并相邻的页,组成更大的页,缓解内存碎片的问题。

class PageCache
{
public:static PageCache* GetInstance(){return &_sInst;}// 获取从对象到span的映射Span* MapObjectToSpan(void* obj);// 释放空闲span回到Pagecache,并合并相邻的spanvoid ReleaseSpanToPageCache(Span* span);// 获取一个k页的spanSpan* NewSpan(size_t k);std::mutex _pageMtx;
private:SpanList _spanLists[NPAGES];ObjectPool<Span> _spanPool;//std::unordered_map<PageID, Span*> _idSpanMap;//std::map<PageID, Span*> _idSpanMap;TCMalloc_PageMap1<32 - PAGE_SHIFT> _idSpanMap;PageCache(){}PageCache(const PageCache&) = delete;static PageCache _sInst;
};

申请内存:

  1. 当central cache向page cache申请内存时,page cache先检查对应位置有没有span,如果没有则向更大页寻找一个span,如果找到则分裂成两个。比如:申请的是4page,4page后面没有挂span,则向后面寻找更大的span,假设在10page位置找到一个span,则将10page span分裂为一个4page span和一个6page span。
  2. 如果找到128 page都没有合适的span,则向系统使用mmap、brk或者是VirtualAlloc等方式申请128page span挂在自由链表中,再重复1中的过程。
  3. 需要注意的是central cache和page cache 的核⼼结构都是spanlist的哈希桶,但是他们是有本质区别的,central cache中哈希桶,是按跟thread cache⼀样的⼤⼩对⻬关系映射的,他的spanlist中挂的span中的内存都被按映射关系切好链接成⼩块内存的⾃由链表。⽽page cache 中的spanlist则是按下标桶号映射的,也就是说第i号桶中挂的span都是i⻚内存。

释放内存:

  1. 如果central cache释放回一个span,则依次寻找span的前后page id的span,看是否可以合并,如果合并继续向前寻找。这样就可以将切小的内存合并收缩成大的span,减少内存碎片。

映射关系、FreeList、SpanList

// 自由链表的哈希桶跟对象大小的映射关系
// 小于等于MAX_BYTES,就找thread cache申请
// 大于MAX_BYTES,就直接找page cache或者系统堆申请
static const size_t MAX_BYTES = 256 * 1024;
// thread cache 和 central cache自由链表哈希桶的表大小
static const size_t NFREELIST = 208;
// page cache 管理span list哈希表⼤⼩
static const size_t NPAGES = 129;
// 页大小转换偏移,即一页定义为2^13,也就是8KB
static const size_t PAGE_SHIFT = 13;// ⻚编号类型,32位下是4byte类型,64位下是8byte类型
#ifdef _WIN64
typedef unsigned long long PageID;
#elif _WIN32
typedef size_t PageID;
#endif// 直接去堆上按⻚申请空间
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32void* ptr = VirtualAlloc(0, kpage * (1 << 12), MEM_COMMIT | MEM_RESERVE,PAGE_READWRITE);
#else// linux下brk mmap等
#endifif (ptr == nullptr){throw std::bad_alloc();}return ptr;
}inline static void SystemFree(void* ptr)
{
#ifdef _WIN32VirtualFree(ptr, 0, MEM_RELEASE);
#else// sbrk unmmap等
#endif
}// 获取内存对象中存储的头4 or 8字节值,即链接的下⼀个对象的地址
static void*& NextObj(void* obj)
{return *(void**)obj;
}
// 管理切分好的小对象的自由链表
class FreeList
{
public:void Push(void* obj){assert(obj);// 头插//*(void**)obj = _freeList;NextObj(obj) = _freeList;_freeList = obj;_size++;}void* Pop(){assert(_freeList);// 头删void* obj = _freeList;_freeList = NextObj(obj);_size--;return obj;}void PushRange(void* start, void* end,size_t n){NextObj(end) = _freeList;_freeList = start;_size += n;}void PopRange(void*& start, void*& end, size_t n){assert(n <= _size);start = _freeList;end = start;for (size_t i = 0; i < n - 1; i++){end = NextObj(end);}_freeList = NextObj(end);NextObj(end) = nullptr;_size -= n;}bool Empty(){return _freeList == nullptr;}size_t& MaxSize(){return _maxSize;}size_t Size(){return _size;}private:void* _freeList = nullptr;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)/*size_t _RoundUp(size_t bytes, size_t align){size_t alignSize;if (bytes % align != 0){alignSize = (bytes / align + 1) * align;}else{alignSize = bytes;}return alignSize;}*/static inline size_t _RoundUp(size_t bytes, size_t align){return ((bytes+align - 1) & ~(align - 1));}// 对齐大小计算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 bytes, size_t align_shift){if (bytes % align_shift == 0){return bytes / align_shift - 1;}else{return bytes / align_shift;}}*/static inline size_t _Index(size_t bytes, size_t align_shift){return ((bytes + (static_cast<unsigned long long>(1) << align_shift) - 1) >> align_shift) - 1;}// 计算映射的哪一个自由链表桶static inline size_t Index(size_t bytes){assert(bytes <= MAX_BYTES);// 每个区间有多少个链static int group_array[4] = { 16,56,56,56 };if (bytes <= 128){return _Index(bytes, 3);}else if (bytes <= 1024){return _Index(bytes - 128, 4) + group_array[0];}else if (bytes <= 8 * 1024){return _Index(bytes - 1024, 7) + group_array[1] + group_array[0];}else if (bytes <= 64 * 1024){return _Index(bytes - 8 * 1024, 10) + group_array[2] + group_array[1] + group_array[0];}else if (bytes <= 256 * 1024){return _Index(bytes - 64 * 1024, 13) + group_array[3] + group_array[2] + group_array[1] + group_array[0];}else{assert(false);}return -1;}// ⼀次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;}// 计算⼀次向系统获取⼏个⻚// 单个对象 8byte// ...// 单个对象 256KBstatic 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;}
};// Span管理一个跨度的大块内存
// 管理以页为单位的大块内存
// 管理多个连续页大块内存跨度结构
struct Span
{PageID _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; // 是否在被使用
};// 带头双向循环链表
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->_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;
public:std::mutex _mtx; // 桶锁
};

四、项目测试

测试时一定要确保编译器在release情况下,而不是debug!!!

五、项目总结

【优点】

  1. 增加动态申请的效率
  2. 减少陷入内核的次数
  3. 减少系统内存碎片
  4. 提升内存使用率
  5. 尽量减少锁竞争
  6. 应用于多核多线程场景

【不足】

  1. 当前实现的项目中并没有完全脱离malloc,比如在内存池自身数据结构的管理中,如SpanList中的span等结构,还是使用的new Span,new的底层使用的是malloc,所以还不足以替换malloc,因为我们本身没有完全脱离它。
  2. 平台及兼容性。linux等系统下面,需要将VirtualAlloc替换为brk等。

六、项目源码

ConcurrentMemoryPool/ConcurrentMemoryPool · 胡家豪/项目 - 码云 - 开源中国 (gitee.com)

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

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

相关文章

OpenCV 图像预处理—图像金字塔

文章目录 相关概念高斯金字塔拉普拉斯金字塔应用 构建高斯金字塔为什么要对当前层进行模糊&#xff1f;1. 平滑处理2. 减少混叠&#xff08;Aliasing&#xff09;3. 多尺度表示4. 图像降采样 举个栗子创建高斯金字塔和拉普拉斯金字塔&#xff0c;并用拉普拉斯金字塔恢复图像 相…

【VUE】个人记录:父子页面数据传递

1. 父传子 在父页面中&#xff0c;调用子页面的组件位置处&#xff0c;通过“&#xff1a;”进行参数传递 <child-component :childData"parentData"></child-component>parentData对象&#xff0c;需要在父页面的data中进行初始化声明 在子页面中&am…

百易云资产管理运营系统 comfileup.php 文件上传致RCE漏洞复现(XVE-2024-18154)

0x01 产品简介 百易云资产管理运营系统,是专门针对企业不动产资产管理和运营需求而设计的一套综合解决方案。该系统能够覆盖资产的全生命周期管理,包括资产的登记、盘点、评估、处置等多个环节,同时提供强大的运营分析功能,帮助企业优化资产配置,提升运营效率。 0x02 漏…

为RTEMS Raspberrypi4 BSP添加SPI支持

为RTEMS Raspberrypi4 BSP添加SPI支持 主要参考了dev/bsps/shared/dev/spi/cadence-spi.c RTEMS 使用了基于linux的SPI框架&#xff0c;SPI总线驱动已经在内核中实现。在这个项目中我需要实习的是 RPI4的SPI主机控制器驱动 SPI在RTEMS中的实现如图&#xff1a; 首先需要将S…

Profinet从站转TCP/IP协议转化网关(功能与配置)

如何将Profinet和TCP/IP网络连接通讯起来呢?近来几天有几个朋友问到这个问题&#xff0c;那么作者在这里统一说明一下。其实有一个不错的设备产品可以很轻易地解决这个问题&#xff0c;名为JM-DNT-PN。接下来作者就从该设备的功能及配置详细说明一下。 一&#xff0c;设备主要…

Python:随机数、随机选择的应用

step1:导入 导入的random相当于是创建了random文件里的的一个对象 import random random() 产生0~1随机数 randint(a,b)产生a~b的整数 闭区间&#xff0c;可以取到a,b random.choice(touple_name)从touple_name&#xff08;数组、列表..&#xff09;中随机选择元素 import rand…

JSP内置对象及作用域

Request 存东西ResponseSession 存东西Application [ SerlvetContext ] 存东西config [ SerlvetConfig ]out/targetpage 不用了解exception <% page contentType"text/html;charsetUTF-8" language"java" %> <html> <head><title>…

DBeaver使用SQL脚本编辑器

文章目录 1 新建脚本2 选择数据库3 编写脚本【按行执行】参考 1 新建脚本 2 选择数据库 3 编写脚本【按行执行】 光标放到需要执行的行上&#xff0c;点击【最上面的按钮】 或者选中某片代码&#xff0c;然后执行 也可以编写一个脚本然后执行 参考 dbeaver安装和使用教程 …

LeetCode 热题 HOT 100 (011/100)【宇宙最简单版】

【图论】No. 0200 岛屿数量 【中等】&#x1f449;力扣对应题目指路 希望对你有帮助呀&#xff01;&#xff01;&#x1f49c;&#x1f49c; 如有更好理解的思路&#xff0c;欢迎大家留言补充 ~ 一起加油叭 &#x1f4a6; 欢迎关注、订阅专栏 【力扣详解】谢谢你的支持&#xf…

Chrome谷歌浏览器Console(控制台)显示文件名及行数

有没有这样的困扰&#xff1f;Chrome谷歌浏览器console(控制台)不显示编译文件名及行数? 设置&#xff08;Settings&#xff09;- > 忽略列表&#xff08;lgnore List&#xff09;-> 自定义排除规则&#xff08;Custom exclusion rules&#xff09; 将自定义排除规则…

Skyeye云智能制造企业版源代码全部开放

智能制造一体化管理系统 [SpringBoot2 - 快速开发平台]&#xff0c;适用于制造业、建筑业、汽车行业、互联网、教育、政府机关等机构的管理。包含文件在线操作、工作日志、多班次考勤、CRM、ERP 进销存、项目管理、EHR、拖拽式生成问卷、日程、笔记、工作计划、行政办公、薪资模…

【数据结构】堆,优先级队列

目录 堆堆的性质大根堆的模拟实现接口实现构造方法建堆入堆判满删除判空获取堆顶元素 Java中的PriorityQueue实现的接口构造方法常用方法PriorityQueue注意事项 练习 堆 如果有一个集合K {k0&#xff0c;k1&#xff0c; k2&#xff0c;…&#xff0c;kn-1}&#xff0c;把它的…

【C++】C++入门基础

✨✨欢迎大家来到Celia的博客✨✨ &#x1f389;&#x1f389;创作不易&#xff0c;请点赞关注&#xff0c;多多支持哦&#x1f389;&#x1f389; 所属专栏&#xff1a;C 个人主页&#xff1a;Celias blog~ 目录 一、C简介 二、第一个C程序 三、namespace 命名空间 3.1 na…

UART 通信协议

文章目录 一 简介二 电平标准三 引脚定义四 数据格式五 波特率 一 简介 ​ UART (Universal Asynchronous Receiver/Transmitter)&#xff0c;通用异步收发器&#xff0c;是一种串行、异步、全双工通信协议。 串行&#xff1a;利用一条传输线&#xff0c;将数据一位一位地传送…

一整套开箱即用的前端管理后台解决方案,基于 Vue.js搭配使用 iView UI 组件库形成的,私活神器

前言 在现代Web应用开发中&#xff0c;后台管理系统的构建常常面临诸多挑战&#xff0c;如复杂的权限管理、多语言支持、响应式设计等。现有解-决方案可能存在功能不丰富、定制难度大、开发效率低等问题。 为了解决这些痛点&#xff0c;一款新的软件——iView Admin&#xff…

【Docker】Windows11环境下的安装

前置依赖环境配置 确保虚拟化开启 搜索栏直接搜索如下功能 勾选下面两个选项&#xff0c;确定 重启电脑&#xff0c;以管理员身份打开PowerShell wsl --status wsl --update打开微软应用商店选择一个Ubuntu版本下载并打开 输入一个用户名和密码 然后就可以在Windows下使…

Flink-CDC解析(第47天)

前言 本文主要概述了Flink-CDC. 1. CDC 概述 1.1 什么是CDC&#xff1f; CDC是&#xff08;Change Data Capture 变更数据获取&#xff09;的简称 &#xff0c;在广义的概念上&#xff0c;只要是能捕获数据变更的技术&#xff0c;都可以称之为 CDC。 核心思想是&#xff0c…

昇思MindSpore 应用学习-GAN图像生成-CSDN

模型简介 生成式对抗网络(Generative Adversarial Networks&#xff0c;GAN)是一种生成式机器学习模型&#xff0c;是近年来复杂分布上无监督学习最具前景的方法之一。 最初&#xff0c;GAN由Ian J. Goodfellow于2014年发明&#xff0c;并在论文Generative Adversarial Nets中…

超逼真AI生成电影来了!《泰坦尼克号》AI重生!浙大阿里发布MovieDreamer,纯AI生成电影引爆热议!

视频生成领域的最新进展主要利用了短时内容的扩散模型。然而&#xff0c;这些方法往往无法对复杂的叙事进行建模&#xff0c;也无法在较长时间内保持角色的一致性&#xff0c;而这对于电影等长篇视频制作至关重要。 对此&#xff0c;浙大&阿里发布了一种新颖的分层框架Mov…

图解分布式事务中的2PC与Seata方案

文章目录 文章导图什么是2PC解决传统2PC方案XA方案DTP模型举例&#xff1a;新用户注册送积分总结&#xff1a; Seata方案设计思想执行流程举例&#xff1a;新用户注册送积分 Seata实现2PC事务&#xff08;AT模式&#xff09;前提整体机制写隔离读隔离实际案例理解要点说明核心代…