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

news2024/9/22 11:27:37

目录​​​​​​​

项目介绍

池化技术

内存池 

内存碎片

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 _WIN32
		void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
	#endif
		if (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);
		#endif
			if (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.coloradmin.cn/o/2108036.html

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

相关文章

一种极简的余弦定理证明方法

余弦定理的证明方法有很多种&#xff0c;这里介绍一种极简的证明方法。该方法是本人在工作中推导公式&#xff0c;无意中发现的。证明非常简单&#xff0c;下面简单做下记录。   如上图为任意三角形ABC&#xff0c;以点C为原点&#xff0c;建立直角坐标系&#xff08;x轴方向…

【网络编程通关之路】 Udp 基础回显服务器(Java实现)及你不知道知识原理详解 ! ! !

本篇会加入个人的所谓鱼式疯言 ❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言 而是理解过并总结出来通俗易懂的大白话, 小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的. &#x1f92d;&#x1f92d;&#x1f92d;可能说的不是那么严谨.但小编初心是能让更多人…

vue3中ref自动解包

1.模板中使用 ref 类型的数据&#xff0c;会自动解包&#xff0c;注意需要是顶级的ref <template> <!-- 自动解包--><div>{{ name }}</div> </template><script setup> import { ref} from vue const name ref(hello) </script>下…

com.alibaba.fastjson.JSONArray循环引用导致{“$ref“:“$[0]“}

发一个库存~ 在for循环中将对象add到.JSONArray中&#xff0c;arr.toJSONString()&#xff0c;输出的结果如下&#xff1a; [{"sex":"男","age":"10","name":"张三"},{"$ref":"$[0]"},{&quo…

Java synchronized 原理

Synchronized使用 synchronized关键字可使用在方法上或代码块上表示一段同步代码块&#xff1a; public class SyncTest {public void syncBlock(){synchronized (this){System.out.println("hello block");}}public synchronized void syncMethod(){System.out.pr…

小白入门LLM大模型最牛X教程------上交《动手学大模型应用开发》!

本项目是一个面向小白开发者的大模型应用开发教程&#xff0c;旨在结合个人知识库助手项目&#xff0c;通过一个课程完成大模型开发的重点入门&#xff0c;涵盖了大模型应用开发的方方面面&#xff0c;主要包括&#xff1a; 教程一共有七章内容&#xff1a; 《动手学大模型》…

13.5 告警静默

本节重点介绍 : 静默应用场景页面创建api接口创建查看 静默 作用 先告警后静默&#xff1a;持续发送的告警停止发送先配置静默&#xff1a;上线或者运维操作会导致触发一大波告警&#xff0c;提前创建静默消息。防止告警风暴 静默接口 /api/v2/silences 调用静默的代码 …

Leetcode8.字符串转换整数 -codetop

代码&#xff08;首刷看解析 2024年9月5日&#xff09; class Solution { public:int myAtoi(string str) {unsigned long len str.length();// 去除前导空格int index 0;while (index < len) {if (str[index] ! ) {break;}index;}if (index len) {return 0;}int sign …

idea插件开发之bean复制插件

背景 周末在家无事做&#xff0c;顺手开发了一个之前一直想要做的插件&#xff0c;那就是bean复制插件。 在项目中&#xff0c;由于代码分层设计&#xff0c;对于同样一个数据我们通常会定义不同层的实体&#xff0c;例如xxxEntity、xxxDTO、xxxVO等&#xff0c;这些不同的实…

echarts地图绘制并实现下钻功能

本文参考网址 使用echarts地图需要先准备好echarts地图渲染需要的json数据&#xff0c;数据可以从阿里云地址中下载自己需要的&#xff0c;下载之后直接引入即可使用&#xff0c;本文针对全国地图做一个简单的demo 阿里云界面如图 // 1、准备echarts地图容器<div class&…

如何借助AI快速筛选和整理文献?

AIPaperGPT&#xff0c;论文写作神器~ https://www.aipapergpt.com/ 在撰写毕业论文时&#xff0c;文献综述是必不可少的部分。它不仅为你的研究提供理论背景&#xff0c;还展示了你对研究领域的深入理解。然而&#xff0c;文献综述的撰写过程常常让学生感到头疼&#xff0c;…

基于JAVA+SpringBoot+Vue的大学校园回忆录系统

基于JAVASpringBootVue的大学校园回忆录系统 前言 ✌全网粉丝20W,csdn特邀作者、博客专家、CSDN[新星计划]导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末附源码下载链接&#x1f345; …

ElasticSearch-聚合操作

聚合的分类 aggsMetric Aggregation min, max, avg, sumstats, cardinality Bucket Aggregation terms ordertext -> fielddatarangehistogramtop_hits Pipeline Aggregation min_bucketstats_bucketpercentiles_bucketcumulative_sum 聚合的作用范围 Filter, Post Filter,…

5.1.数据结构-c/c++二叉树详解(上篇)(遍历,几种二叉树)

本章所有代码请见&#xff1a;5.3.数据结构-c/c二叉树代码-CSDN博客 目录 一. 二叉树的基本介绍 1.2 满二叉树 1.3 完全二叉树 1.4 搜索二叉树 1.5 平衡二叉搜索树 二. 二叉树的常用操作 2.1 二叉树的定义 2.2 创建一个新的节点 2.3 构建一颗树 2.5 销毁一棵树 三.…

One-Shot Imitation Learning with Invariance Matching for Robotic Manipulation

发表时间&#xff1a;5 Jun 2024 论文链接&#xff1a;https://readpaper.com/pdf-annotate/note?pdfId2408639872513958656&noteId2408640378699078912 作者单位&#xff1a;Rutgers University Motivation&#xff1a;学习一个通用的policy&#xff0c;可以执行一组不…

Linux学习笔记6 值得一读,Linux(ubuntu)软件管理,搜索下载安装卸载全部搞定!(中)

Linux学习笔记5 值得一读&#xff0c;Linux&#xff08;ubuntu&#xff09;软件管理&#xff0c;搜索下载安装卸载全部搞定&#xff01;(上)-CSDN博客 一、前文回顾 上一篇文章我们了解了软件管理的基本概念和软件管理的几种常用工具。我们了解了软件包是由什么形式存在&#…

srt字幕文件怎么制作?分享几个简单步骤,新手必学

srt字幕文件怎么制作&#xff1f;随着短视频平台的发展&#xff0c;现在很多小伙伴喜欢用视频记录生活&#xff0c;分享美好瞬间。在将视频上传到视频平台的时候&#xff0c;我们需要对视频进行剪辑处理。而字幕的使用对提高视频内容的可理解性与传播性变得愈发重要。srt字幕文…

OpenCV 旋转矩形边界

边界矩形是用最小面积绘制的&#xff0c;所以它也考虑了旋转。使用的函数是**cv.minAreaRect**()。 import cv2 import numpy as npimgcv2.imread(rD:\PythonProject\thunder.jpg) img1cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) print(img.dtype) ret,threshcv2.threshold(img1,1…

基于SpringBoot+Vue的美术馆管理系统(带1w+文档)

基于SpringBootVue的美术馆管理系统(带1w文档) 基于SpringBootVue的美术馆管理系统(带1w文档) 本课题研究和开发美术馆管理系统管理系统&#xff0c;让安装在计算机上的该系统变成管理人员的小帮手&#xff0c;提高美术馆管理系统信息处理速度&#xff0c;规范美术馆管理系统信…

【高等数学学习记录】集合

1 知识点 1.1 集合的概念 集合 指具有某种特定性质的事物的总称。集合的元素 组成集合的事物称为集合的元素&#xff08;简称元&#xff09;。有限集、无限集 含有限个元素的集合&#xff0c;则称为有限集&#xff1b;反之&#xff0c;称为无限集。子集 设 A A A、 B B B是两…