高并发内存池(一):项目介绍与定长内存池的实现

目录​​​​​​​

项目介绍

池化技术

内存池 

内存碎片

malloc工作原理

定长内存池

申请内存

释放内存

定位new 

VirtualAlloc函数

封装VirtualAlloc

定长内存池的最终代码


项目介绍

项目原型:goole的开源项目tcmalloc(Thread-Caching Malloc)

项目目标:实现高效的多线程内存管理,用于替代系统的内存分配相关的函数(malloc等)

涉及技术栈:C/C++、数据结构(链表、哈希桶)、操作系统的内存管理、单例模式、多线程、互斥锁、慢调节算法

池化技术

基本概念:程序提前向系统申请过量的资源,然后自行管理,从而减少每次申请资源时的开销,提高程序运行效率(比如线程池的主要思想就是:先启动若干数量的线程,让它们处于睡眠状态,当接收到客户端的请求时,唤醒池中的某个睡眠的线程,让它来处理客户端的请求,当处理完请求后,该线程继续进入睡眠状态)

内存池 

基本概念:与线程池的原理一样,内存池是程序预先从操作系统中申请一块足够大的内存,然后当程序中需要申请内存时,不是直接向操作系统申请,而是直接从内存池中获取;当程序释放内存时,并不是真正将内存返回给操作系统,而是返回给内存池,当程序退出或到达特定时间时,内存池才将之前申请的内存真正释放

补充:内存池除了要解决内存申请的效率问题,还要解决内存碎片问题

内存碎片

基本概念:内存碎片分为内碎片和外碎片,内碎片指系统分配的但没用完的内存,外碎片指系统还可分配的内存

问题分析:如下图所示,在申请一块300Byte的连续地址空间时,由于返还所产生的两个外碎片的地址空间并不相连所以会导致申请失败,同时对于内碎片而言只会使用20Byte但系统分配了1000Byte那么就会造成大量的浪费

总结:外碎片过多会导致虽然总内存足够,但内存空间可能不连续,不能满足一些较大的内存分配申请;内部碎片过多会导致分配出去的内存浪费

malloc工作原理

基本概念:C/C++中动态申请内存都是通过malloc去申请内存,但实际上malloc就是一个内存池,调用malloc就相当于向操作系统“批发”一大批内存空间,然后“零售”给程序使用,当全部“售完”或程序有更大的内存需求时,再根据需求向操作系统“进货”,各个平台的malloc的实现方式都是不同的

定长内存池

基本概念:提前开辟一块固定大小的内存块,基于自由链表实现对该大块内存的使用和释放,同时放弃使用malloc向操作系统申请内存的方式

申请内存

1、起始时_memory指向的大块内存为空,需要申请(这里我们规定申请128Kb),然后每次为T类型对象分配所需要的内存后,向后移动_memory指向的位置,并返回一个指向申请到的内存的指针

class ObjectPool
{
public://申请内存T* New(){T* obj = nullptr;if(_memory == nullptr){_memory = (char*)malloc(128 * 1024);if(_memory == nullptr){throw std::bad_alloc();//申请失败就抛异常}}obj = (T*)_memory;_memory += sizeof(T);return obj;}
private:char *_memory = nullptr;//指向申请的大块内存的指针
}

2、提前申请的128Kb大小的内存块被用完时,再次申请时_memory+=sizeof(T)就会越界访问,所以当剩余内存_remainBytes < sizeof(T)时就需要重新申请新大块内存

class ObjectPool
{
public://申请内存T* New(){T* obj = nullptr;//剩余内存不够一个T对象大小时,重新开大块空间if (_remainBytes < sizeof(T)){    _remainBytes = 128 * 1024;//初始设定_remainBytes为128Kb大小,其实也是设定了每次要重新申请的大块内存的大小为128Kb_memory = (char*)malloc(_remainBytes);if(_memory == nullptr){throw std::bad_alloc();//申请失败就抛异常}}obj = (T*)_memory;_memory += sizeof(T);_remainBytes -= sizeo(T);//每次分配后重新结算剩余字节数return obj;}
private:char *_memory = nullptr;//指向申请的大块内存的指针size_t _remainBytes = 0;//大块内存剩余的字节数,缺省值设置为0是为了保证第一次申请时可以直接进入if (_remainBytes < sizeof(T))中去开辟内存void* _freelist = nullptr;//指向自由链表
}

3、我们不能逮着一个内存块狠用,也要将归还的内存块利用起来

#define MAX_TYPES 256*1024;//定义最大的内存为256Kb
class ObjectPool
{
public://申请内存T* New(){T* obj = nullptr;if(_freelist != nullptr){    //头删void* next = *((void**)_freelist);//next指向自由链表的第二个结点obj = _freelist;_freelist = next;return obj;//返回指向从自由链表中分配的结点的指针}else{//剩余内存不够大时......(后续不变)//移动_memory....}}
private:char *_memory = nullptr;//指向申请的大块内存的指针size_t _remainBytes = 0;//大块内存剩余的字节数void* _freelist = nullptr;//指向自由链表
}

4、若T对象占用的字节数小于存放下一个结点地址的字节数,如果还是要多少分配多少,就会导致无法链接其它结点,因此我们要保证即使T对象本身所需内存过小也能记录下一个结点的位置

//仅需要在这里新增一行判断,其余位置不变
obj = (T*)_memory;size_t objsize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);//T对象需要的内存大小小于当前环境下一个指针的大小,则最少给一个指针的大小_memory += sizeof(T);
_remainBytes -= sizeo(T);//每次分配后重新结算剩余字节数

释放内存

1、当T类型对象使用完它所申请的内存后,需要将不用的内存返回,这些被返回的内存会被挂在自由链表上,当自由链表为空时就要先头插,同时我们试图让每个结点的前n个字节存放下一个结点的地址(即指针)

注意事项:32下指针4字节大小和64位环境下指针8字节大小且int均为4字节,如果在32位机器下使用*(int*)obj 令obj指向的内存结点的前4字节存放下个结点的地址是没问题的,但是如果是64位环境,指针占8字节解引用后仍只能获取前4个字节,即获取的地址是实际的一半就会出问题,所以我们采用解引用二级指针的方式,这样就不需要我们额外的判断当前程序运行时所处的环境了(解引用得到的都是一级指针,32位下一级指针表示4字节就让前4字节为空,64位下一级指针表示8字节就让前8字节为空)

class ObjectPool
{
public://申请内存T* New(){省略.....};//回收内存void Delete(T* obj)//传入指向要回收的对象的指针{/*可以不考虑链表是否为空的情况,直接头插即可,因为_freelist起始为空(不信自行带入测试)if(_freelist == nullptr)//链表为空就先头插{_freelist = obj;//*(int*)obj = nullptr;//淘汰*(void**)obj = nullptr;}  else//头插{*(void**)obj = _freelist;_freelist = obj;}*///修改后*(void**)obj = _freelist;_freelist = obj;}private:char* _memory = nullptr;size_t _remainBytes = 0;void* _freelist = nullptr;//指向自由链表的指针
}

定位new 

功能:在已分配好的一块内存空间中调用某对象的构造函数初始化一个该对象,在实际应用中,定位new一般是配合内存池使用的,因为内存池分配出来的空间没有初始化,因此如果需要在这块内存池分配出来的空间上构造自定义类型的对象,需要使用定位new显式调用构造函数构造目标对象

格式:

格式一:new (place_address) type 
格式二:new (palce_address) type (initializer_list)
  • place_address:指向待构造对象的指针
  • type:待构造对象的类型
  • initializer_list:待构造对象的初始化列表

注意事项:

  1. 需要手动管理内存:使用定位new时,程序员必须先分配内存,并确保这块内存足够大,能够容纳将要构造的对象。此外,还需要负责这块内存的释放。

  2. 不进行内存分配:定位new只调用对象的构造函数,不会像new运算符那样分配内存。因此,如果提供的内存不足,会引发未定义行为。

  3. 不能使用默认构造函数:如果没有为定位new提供的内存地址提供一个合适的构造函数,编译器将无法调用默认构造函数,除非该构造函数已经在类定义中显式声明。

  4. 需要显式调用析构函数(重要):由于定位new不包括分配和释放内存的代码,因此必须显式地调用对象的析构函数来销毁对象,以避免内存泄漏。

  5. 处理数组:如果使用定位new来创建一个对象数组,那么构造每个对象时都需要分别调用定位new,同时在数组被销毁时,需要为每个对象分别调用析构函数

VirtualAlloc函数

基本概念:为了使得定长内存池不使用malloc,我们可以使用Windows和Linux均有提供的直接向系统申请以页为单位的大块内存的接口,Windows是VirtualAlloc,Linux是brk()和mmap()  

参考链接:VirtualAlloc 函数 (memoryapi.h) - Win32 apps | Microsoft Learn 

函数原型: 

LPVOID VirtualAlloc([in, optional] LPVOID lpAddress,[in]           SIZE_T dwSize,[in]           DWORD  flAllocationType,[in]           DWORD  flProtect
);
  • lpAddress可选参数,指定希望分配的虚拟内存的起始地址。若传入 NULL,系统自动分配
  • dwSize指定要分配的内存区域大小,单位为字节
  • flAllocationType标志位(可多个),我们这里使用了MEM_COMMIT | MEM_RESERVE这两个标志位结合,这表示VirtualAlloc函数会尝试为调用进程分配一块指定大小的内存区域,并立即为这块内存分配物理存储器。这样做的好处是确保了内存区域既不会被其他分配占用,也可以立即被访问
  • flProtect指定分配的内存页面的保护属性,我们这里选择PAGE_READWRITE表示可读写访问

封装VirtualAlloc

基本概念:通过对VirtualAlloc函数进行封装,我们就可以写出一个避开malloc直接向操作系统申请内存的自定义函数,就可以将后续使用malloc的场景直接替换为SystemAlloc函数

//这里使用Windows开发环境
inline static void* SystemAlloc(size_t kpage)//kpage表示页数
{#ifdef _WIN32void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);#endifif (ptr == nullptr)throw std::bad_alloc();//抛异常return ptr;
}
  • static inline的解释: SystemAlloc函数被建议内联展开,并且它是一个文件内部的静态函数,它的作用域被限定在了定义它的文件内
  • VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE):在进程的虚拟地址空间中申请一块大小为 kpage * 8192 字节的区域,这块内存既被预留也被提交,并且具有可读写的属性

定长内存池的最终代码

template<class T>//模板参数T
class ObjectPool
{
public://封装VirtualAlloc跳过malloc直接向操作系统申请以页为单位的内存inline static void* SystemAlloc(size_t kpage)//kpage表示页数{#ifdef _WIN32//使用Windows开发环境时可以使用Windows提供的VirtualAlloc函数void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);#endifif (ptr == nullptr)throw std::bad_alloc();//抛异常return ptr;}//为T对象构造一大块内存空间T* New(){T* obj = nullptr;if (_freelist != nullptr){//头删void* next = *((void**)_freelist);//next指向自由链表的第二个结点obj = _freelist;_freelist = next;return obj;//返回指向从自由链表中分配的结点的指针}else//自由链表没东西才会去用大块内存{//剩余内存不够一个T对象大小时,重新开大块空间if (_remainBytes < sizeof(T)){_remainBytes = 128 * 1024;//初始设定_remainBytes为128Kb大小,其实也是设定了每次要重新申请的大块内存的大小为128Kb_memory = (char*)SystemAlloc(_remainBytes >> 13);//向SystemAlloc函数传递的是要向操作系统申请的页数而不是整体的字节数(在SystemAlloc函数中会再次转换为具体字节数)if (_memory == nullptr){throw std::bad_alloc();//申请失败就抛异常}}obj = (T*)_memory;size_t objsize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);//无论T对象需要的内存大小有多大,则每次分配的内存应该大于等于当前环境下一个指针的大小,从而保证可以顺利存放下一个结点的地址_memory += sizeof(T);_remainBytes -= sizeo(T);//每次分配后重新结算剩余字节数}//定位new,显示调用T的构造函数初始化new(obj)T;return obj;}//回收内存void Delete(T* obj)//传入指向要回收的对象的指针{//显示调用析构函数清理对象obj->~T();/*可以不考虑链表是否为空的情况,直接头插即可,因为_freelist起始为空(不信自行带入测试)if(_freelist == nullptr)//链表为空就先头插{_freelist = obj;//*(int*)obj = nullptr;//淘汰*(void**)obj = nullptr;}  else//头插{*(void**)obj = _freelist;_freelist = obj;}*///修改后*(void**)obj = _freelist;_freelist = obj;}private:char* _memory = nullptr;//指向大块内存的指针size_t _remainBytes = 0;//大块内存在切分过程中剩余字节数void* _freelist = nullptr;//自由链表,因为借用内存的对象的类型是不确定的所以要使用void*
};

~over~

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

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

相关文章

CIOE中国光博会&电巢科技即将联办“智能消费电子创新发展论坛”

在科技浪潮汹涌澎湃的当下&#xff0c;从通信领域的高速光传输&#xff0c;到消费电子中的高清显示与先进成像技术&#xff0c;光电技术的应用范围不断拓展且日益深化。而AIGC 凭借其丰富的内容供给与个性化反馈能力&#xff0c;正为新一代消费电子及智能穿戴产品开辟崭新的发展…

前端请求的路径baseURL怎么来的 ?nodejs解决cors问题的一种方法

背景&#xff1a;后端使用node.js搭建&#xff0c;用的是express 前端请求的路径baseURL怎么来的 &#xff1f; 前后端都在同一台电脑上运行&#xff0c;后端的域名就是localhost&#xff0c;如果使用的是http协议&#xff0c;后端监听的端口号为3000&#xff0c;那么前端请求…

S3C2440开发板:时钟,PWM定时器控制蜂鸣器发声

时钟 时钟和电源管理模块由三部分组成&#xff1a;时钟控制&#xff0c;USB 控制和电源控制。 S3C2440A 中的时钟控制逻辑可以产生必须的时钟信号&#xff0c;包括 CPU 的 FCLK&#xff0c;AHB 总线外设的 HCLK 以及 APB 总线外设的 PCLK。S3C2440A 包含两个锁相环&#xff08…

VBA进行excel坐标转换

在Excel里利用坐标绘图时&#xff0c;可以比较容易想到采用数据透视表&#xff0c;但是数据透视表生成的图不可更改&#xff0c;因此本案例采用VBA进行坐标变换而不改变原始值来转换图像&#xff0c;即实现图像的左右翻转和上下翻转&#xff0c;如下图所示&#xff0c;选择map的…

fastadmin 文件上传腾讯云

1-安装腾讯云SDK composer require qcloud/cos-sdk-v5 2-腾讯云配置 <?phpnamespace app\common\controller;use Qcloud\Cos\Client; use think\Controller; use think\Db;class Tencent extends Controller {/*** 上传文件* param $config* param $key* return array*/p…

Linux下快速判断当前终端使用的是bash or csh

在Linux下设置环境变量的时候&#xff0c;可能你也遇到过export: Command not found一类的错误。这是因为当前终端使用的不是bash&#xff0c;如何快速判断当前终端使用的是哪种类型的shell呢&#xff1f; echo $0判断shell类型 最简单的方法就是在终端输入echo $0&#xff0…

每日一题,零基础入门FPGA——工程师在线精讲来咯

传送门&#xff1a;zzfpga.com/StudentPlatform/Train/StudentArticleDetails?id149

M3U8工作原理以及key解密视频流详解

文章目录 前言一、M3U8是什么&#xff1f;二、HLS—M3U8的工作原理1.分段视频流2.生成播放列表3.客户端请求和解析4.片段下载和播放 三、.m3u8文件内部是什么样的&#xff1f;四、简单介绍下AES-128算法五、拿到KEY后如何去解密&#xff1f;1.手动解密.ts文件2.前人栽树&#x…

spring security 如何解决跨域的

一、什么是 CORS CORS(Cross-Origin Resource Sharing) 是由 W3C制定的一种跨域资源共享技术标准&#xff0c;其目就是为了解决前端的跨域请求。在JavaEE 开发中&#xff0c;最常见的前端跨域请求解决方案是早期的JSONP&#xff0c;但是JSONP 只支持 GET 请求&#xff0c;这是一…

深度学习从入门到精通——基于unet++算法实现细胞分割

模型定义 import torch from torch import nn__all__ [UNet, NestedUNet]class VGGBlock(nn.Module):def __init__(self, in_channels, middle_channels, out_channels):super().__init__()self.relu nn.ReLU(inplaceTrue)self.conv1 nn.Conv2d(in_channels, middle_channe…

FPGA速度优化

速度优化 文章目录 速度优化前言一、时序优化1.1 减少关键路径上的时序1.1.1 关键路径重组1.1.2 解决扇出问题1.1.3 路径上插入寄存器1.1.4 寄存器平衡1.1.5 并行结构1.1.6 消除代码优先级 总结 前言 速度优化&#xff0c;主要就是设计时序进行优化 吞吐量&#xff1a;每个时…

web渗透:RCE漏洞

RCE漏洞&#xff0c;即远程代码执行漏洞&#xff0c;是一种安全缺陷&#xff0c;它允许攻击者通过网络在目标系统上执行任意代码。一旦成功利用&#xff0c;攻击者可以完全控制受影响的系统&#xff0c;包括读取敏感数据、安装恶意软件、修改系统配置等。RCE漏洞通常发生在应用…

数据结构---双向链表---循环链表---栈

目录 一、双向链表 1.1.创建双向链表 1.2.头插法 1.3.尾插法 1.4.查询节点 1.5.修改节点 1.6.删除节点 1.7.打印节点 1.8.销毁链表 二、循环链表 2.1.单循环链表 2.2.双循环链表 三、栈 3.1.顺序栈 1.创建栈 2.判断栈是否满 3.判断栈是否为空 4.进栈 5.出栈…

SAP 生产订单工序删除状态撤回简介

SAP 生产订单工序删除状态撤回简介 一、业务场景二、处理办法三、系统控制一、业务场景 生产订单正常没有按工序分配物料,系统会自动会把物料分配到第一道工序中 生产订单中的0010工序中对应的组件的栏位被标识,表示有物料分配到了0010的工序中,正常情况下0010的工序被分配…

【微服务】springboot 自定义注解+反射+aop实现动态修改请求参数

目录 一、前言 二、动态修改接口请求参数的场景 2.1 动态修改请求参场景汇总 2.1.1 数据格式标准化 2.1.2 安全需要 2.1.3 参数校验与默认值设定 2.1.4 数据隐私保护 2.1.5 适配不同客户端 2.1.6 统计与监控 2.1.7 高级功能特性 三、springboot 使用过滤器和拦截器动…

Oracle rac模式下undo表空间爆满的解决

文章目录 前言一、确认对应实例的undo表空间二、确认对应实例undo的文件位置三、确认回滚段使用情况四、检查undo segment状态五、创建新的undo表空间并进行切换六、等待原undo表空间segment状态变更为offline七、删除原undo表空间以及数据文件 前言 一、确认对应实例的undo表空…

【云原生】Helm来管理Kubernetes集群的详细使用方法与综合应用实战

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全…

Seata环境搭建

1、Seata下载&#xff1a; 1.下载地址 2.下载的版本 2、Seata参数配置参考&#xff1a; 各种seata参数官网参考 3、Seata安装部署&#xff1a; 3.1.Seata新手部署指南: 3.2.在mysql8.0数据库里面建库建表 a.建数据库&#xff1a; create database seata; use seata;b.建…

PVN3D(一)代码框架

在windows上配置pvn3d的环境一直配不成功&#xff0c;主要卡在了与C联合编译上&#xff0c;不知道如何处理了。索性先看看代码&#xff0c;竟然发现与论文中的代码对应上了。希望这一段时间把环境配置好。 1.论文中的网络结构 1.RGB图像特征&#xff0c;通过CNN提取特征。深度…

【排序算法】快速排序升级版--三路快排详解 + 实现(c语言)

&#x1f31f;&#x1f31f;作者主页&#xff1a;ephemerals__ &#x1f31f;&#x1f31f;所属专栏&#xff1a;算法 目录​​​​​​​ 前言 一、三路快排的整体思路 二、三路快排的具体实现 1.测试数据、交换函数和三数取中法 2.三路快排函数 三、程序全部代码 总…