当前位置: 首页 > news >正文

高并发内存池(七):大块内存的申请释放问题以及配合定长内存池脱离使用new

一、大块内存问题

1.1 问题概述

在当前我们的高并发内存池中只可以处理小于等于MAXBYTES(256*1024)的内存申请和释放,当用户申请或释放的内存块超过MAXBYTES时我们就应该直接向PageCache进行申请和释放了。具体方法如下:

1.2 内存申请

首先当用户申请的内存块超过MAXBYTES时,我们首先将其页对齐,也就是当用户申请的内存块大于256*1024但是小于260*1024(页大小为4kb)时我们直接页对齐到260*1024。代码实现如下:

//static const size_t PAGE_SHIFT = 12;   static inline size_t roundup(size_t size,size_t  align){return (size + align - 1) & ~(align - 1);}static inline size_t _RoundUp(size_t size){//1->128if (size > 0 && size <= 128){return roundup(size, 8);}else if (size <= 1024){return roundup(size, 16);}else if (size <= 8 * 1024){return roundup(size, 128);}else if (size <= 64 * 1024){return roundup(size, 1024);}else if (size <= 256 * 1024){return roundup(size, 8*1024);}else{//大于256*1024以一个页为大小对齐return roundup(size,1<<PAGE_SHIFT);}}

这里不直接将4*1024(4kb)硬编码的好处是,当我们之后修改页大小时只需要修改PAGE_SHIFT即可,无需再对代码进行大规模调整提升我们的开发效率。

在PageCache中最大可以处理的页的大小时NPAGES-1(128页),而ThreadCache中设定的MAXBYTES为64页那么此时用户申请的大内存块的可能情况就有两种:

  1. size>MAXBYTES&&size<(NPAGES<<PAGE_SHIFT)
  2. size>NPAGES<<PAGE_SHIFT

在ConcurrentAlloc.h中只要大于MAXBYTES我们都交给PageCache进行处理,由其中的Newspan对两种情况进行判断:

static void* ConcurrentAlloc(size_t size)
{if (size > MAXBYTES){size_t alignsize = SizeClass::_RoundUp(size);//大内存块直接向PageCache要:size_t kpage = alignsize >> PAGE_SHIFT;PageCache::GetInstance()->_pageMtx.lock();Span* span=PageCache::GetInstance()->GetInstance()->NewSpan(kpage);PageCache::GetInstance()->_pageMtx.unlock();void* ptr = (void*)(span->Page_Id << PAGE_SHIFT);return ptr;}else{// 通过TLS 每个线程无锁的获取自己的专属的ThreadCache对象if (pTLSThreadCache == nullptr){//pTLSThreadCache = new ThreadCache;static ObjectPool<ThreadCache> tcpool;pTLSThreadCache = tcpool.New();}return pTLSThreadCache->Allocate(size);}}

第一种情况说明申请释放操作都可以由PageCache来完成,因为大内存小于128页可以链入链出哈希表也可以进行内存合并等操作。第二种情况因为申请的内存超过了128页我们直接向系统进行申请。

Span* PageCache::NewSpan(size_t pagenum)
{//大于128页直接向堆申请if (pagenum > NPAGES - 1){void* ptr = SystemAlloc(pagenum);PAGE_ID pageid = (PAGE_ID)ptr >> PAGE_SHIFT;Span* bigspan = new Span;bigspan->Page_Id = pageid;bigspan->_n = pagenum;//建立映射关系:_idSpanMap[bigspan->Page_Id] = bigspan;return bigspan;  }//大于MAXYTES但是小于128页直接在PageCache中处理else{assert(pagenum > 0 && pagenum < NPAGES);//首先查看对应下标处有没有Spanif (!SpanLists[pagenum].Empty()){return SpanLists[pagenum].PopFront();}for (size_t i = pagenum = 1; i < NPAGES; i++){if (!SpanLists[i].Empty()){Span* nSpan = SpanLists[i].PopFront();Span* kSpan = new Span;kSpan->Page_Id = nSpan->Page_Id;kSpan->_n = pagenum;nSpan->Page_Id += pagenum;nSpan->_n -= pagenum;SpanLists[nSpan->_n].PushFront(nSpan);//nspan只需要建立首尾页的映射:_idSpanMap[nSpan->Page_Id] = nSpan;_idSpanMap[nSpan->Page_Id + (nSpan->_n - 1)] = nSpan;//这里kspan要交给上层CentralCache了,首先要建立每个页号与该Span的映射关系for (PAGE_ID i = 0; i < kSpan->_n; i++){_idSpanMap[kSpan->Page_Id + i] = kSpan;}kSpan->isUse = true;return kSpan;}}//表示后续整个哈希桶都为空则向堆申请内存:Span* bigspan=new Span;void* ptr = SystemAlloc(NPAGES - 1);bigspan->Page_Id = (PAGE_ID)ptr >> PAGE_SHIFT;bigspan->_n = NPAGES - 1;SpanLists[bigspan->_n].PushFront(bigspan);return NewSpan(pagenum);}}

这里需要注意的是,大块内存申请后创建Span对象后也需要建立映射关系方便之后的内存释放工作。

1.3 内存释放

因为在申请内存块的过程中,两种可能情况都交给PageCache中的Newspan接口统一分类处理了,所以在ConcurrentFree中我们只需要根据内存块的首地址找到对应的Span并交给PageCache处理就好了:

static void ConcurrentFree(void* ptr, size_t size)
{if (size > MAXBYTES){Span* bigspan = PageCache::GetInstance()->MapObjectToSpan(ptr);PageCache::GetInstance()->_pageMtx.lock();PageCache::GetInstance()->ReleaseSpanToPageCache(bigspan);PageCache::GetInstance()->_pageMtx.unlock();}else{assert(pTLSThreadCache);pTLSThreadCache->Deallocate(ptr, size);}
}

此时ReleaseSpanToPageCache就需处理两种可能的情况,第一种情况说明找到的Span可以链入到哈希桶中进行内存合并,第二种情况则只能将大内存块释放给系统并将Span销毁。

void PageCache::ReleaseSpanToPageCache(Span* span)
{//首先向前合并:if (span->_n > NPAGES - 1){//直接释放给堆void* ptr = (void*)(span->Page_Id << PAGE_SHIFT);SystemFree(ptr);delete span;return;}while (1){PAGE_ID previd = span->Page_Id - 1;auto ret = _idSpanMap.find(previd);if (ret == _idSpanMap.end()){//可能系统还未分配所以没有映射关系break;}Span* retSpan = ret->second;if (retSpan->isUse == true){//还在CentralCache中使用:break;}if ((span->_n + retSpan->_n) > NPAGES - 1){//合并后太大无法挂载在PageCache中:break;}span->Page_Id = retSpan->Page_Id;span->_n += retSpan->_n;SpanLists[retSpan->_n].Erase(retSpan);delete retSpan;}//向后合并:while (1){PAGE_ID nextid = span->Page_Id + span->_n;auto ret = _idSpanMap.find(nextid);if (ret == _idSpanMap.end()){//可能系统还未分配所以没有映射关系break;}Span* retSpan = ret->second;if (retSpan->isUse == true){//还在CentralCache中使用:break;}if ((span->_n + retSpan->_n) > NPAGES - 1){//合并后太大无法挂载在PageCache中:break;}span->_n += retSpan->_n;SpanLists[retSpan->_n].Erase(retSpan);delete retSpan;}SpanLists[span->_n].PushFront(span);span->isUse = false;_idSpanMap[span->Page_Id] = span;_idSpanMap[span->Page_Id + (span->_n - 1)] = span;
}

SystemFree的定义如下:

//Commond.h
inline static void SystemFree(void* ptr)
{
#ifdef _WIN32VirtualFree(ptr, 0, MEM_RELEASE);
#else// sbrk unmmap等
#endif
}

二、脱离使用new

2.1 问题概述

我们设计实现定长内存池的初衷是替代系统接口提高内存的分配释放效率,解决内存碎片的问题。但是在我们的代码中还是大量使用了系统new/delete接口,这就会导致以下问题:

1. 锁竞争失控,并发性能暴跌

标准库的 new/delete 内部依赖全局锁保护内存管理数据结构(如空闲块链表)。在高并发场景下(例如数百个线程同时申请 / 释放内存),所有线程会争抢这把全局锁,导致:

  • 大量线程阻塞:多数线程在等待锁的过程中进入休眠状态,CPU 时间浪费在上下文切换上。
  • 吞吐量骤降:原本能并行处理的内存操作被迫串行化,性能可能下降 10 倍甚至 100 倍(这被称为 “锁风暴”)。

例如,在每秒百万级内存分配的场景中,使用 new 可能导致 90% 以上的 CPU 时间消耗在锁竞争上,而非实际业务逻辑。

2. 系统调用开销被无限放大

new 底层依赖 malloc,而 malloc 最终可能调用 brk 或 mmap 等系统调用向内核申请内存。系统调用的开销极高(通常为微秒级,是用户态操作的 100~1000 倍),且:

  • 高频调用不可承受:高并发场景中,每秒可能产生数十万次内存操作,每次都触发系统调用会导致整体性能瘫痪。
  • 内核态瓶颈:内核处理大量内存申请时,可能因页表维护、内存碎片整理等操作进一步放大延迟。

3. 内存碎片急剧恶化

new/delete 作为通用分配器,无法针对高并发场景的 “高频、小块、随机” 内存需求优化,会导致:

  • 外部碎片:内存被分割成大量不连续的小块,即使总空闲内存充足,也可能因找不到连续块而分配失败。
  • 内部碎片:为满足对齐要求或最小块大小,分配的内存块往往大于实际需求(例如申请 10B 却分配 16B),造成内存浪费。

在长期运行的高并发系统(如服务器)中,碎片可能导致内存利用率降至 30% 以下,最终因 “假内存不足” 崩溃。

通过观察代码我们发现,我们的new/delete主要使用在创建或销毁Span对象的相关代码中。为了解决上述问题,我们配合先前设计的定长内存池脱离使用new/delete:

2.2 配合定长内存池

定长内存池主要通过模板参数T解决固定大小的内存块的申请释放问题,这对我们解决上述问题提供了方法。Span对象的创建与销毁主要集中在PageCache中的内存申请和合并过程中,我们首先在PageCache中引入Span大小的定长内存池_spanPool:

ObjectPool<Span> _spanPool;

然后将代码中所有涉及到new/delete Span更改为_spanPool.New()和_spanPool.Delete()即可。

定长内存池的完整代码如下:

template<class T>
class ObjectPool
{
public:T* New(){T* obj = nullptr;//优先检查自由链表中是否存在空闲内存块if (_freelist != nullptr){obj = (T*)_freelist;_freelist = *(void**)_freelist;}else{if (_remandsize < sizeof(T)){_remandsize = 128 * 1024;_memory = (char*)malloc(_remandsize);}obj = (T*)_memory;size_t objsize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);_memory += objsize;_remandsize -= objsize;}new(obj)T;return obj;}void Delete(T* obj){obj->~T();*(void**)obj = _freelist;_freelist = obj;}
private:char* _memory = nullptr;void* _freelist = nullptr;//剩余的内存资源大小size_t _remandsize = 0;
};

手把手教你搭建定长内存池:

高并发内存池(一):手把手教你设计一个定长内存池_创建内存池-CSDN博客

http://www.dtcms.com/a/460825.html

相关文章:

  • 可以为自己的小说建设网站企业官方网站格式
  • 学做静态网站商城设计app网站建设
  • 【Linux系统】线程安全与死锁问题
  • 分布式锁:Redisson的公平锁
  • 精密牙挺在牙齿脱位中的力学控制原理
  • 移动办公型网站开发温州做网站技术员
  • 【SpringAI】第六弹:深入解析 MCP 上下文协议、开发和部署 MCP 服务、MCP 安全问题与最佳实践
  • Unreal开发痛点破解!GOT Online新功能:Lua全监控 + LLM内存可视化!
  • 节后变电站如何通过智能在线监测系统发现「积劳成疾」的隐患?
  • 基于vscode在WSL中配置PlatformIO开发环境
  • C#基础15-线程安全集合
  • 门诊场景评测深度分析报告:医生-病人-测量代理交互对诊断影响机制研究(下)
  • USCTNET:一种用于物理一致性高光谱图像重建的深度展开核范数优化求解器
  • 为什么我的网站没有百度索引量南充市网站建设
  • 常规线扫描镜头有哪些类型?能做什么?
  • 企业级 K8s 深度解析:从容器编排到云原生基石的十年演进
  • 网络产品报价指南--S5735系列交换机
  • 笔记 | 内网服务器通过wifi穿透,设置流量走向
  • 哈尔滨网站建设市场html5网站编写
  • [THREEJS]实战-基础三要素
  • 光谱相机的探测器阵列
  • 怎么更换网站的域名电商公司组织架构图
  • 网上招聘网站开发报告一个简单的网页代码带图片
  • 嵌入式设备轻量级语音识别实战:从STM32到树莓派的智能语音控制
  • AMD KFD的BO设计分析系列6-3: res_cursor--BO物理内存资源的迭代器
  • C#发送邮件到263邮箱服务器教程
  • 淘宝客建网站要钱的吗京东网站建设案例论文
  • Linux环境下Node.js任意版本安装与pnpm、yarn包管理
  • AI问答与搜索引擎:信息获取的现状
  • Element UI表格中根据数值动态设置字体颜色