C++内存管理(3)——内存池

news2025/1/13 13:43:07

1. 默认内存管理函数的不足(为什么使用内存池)

利用默认的内存管理操作符 new/delete 和函数 malloc()/free() 在堆上分配和释放内存会有一些额外的开销。

系统在接收到分配一定大小内存的请求时,首先查找内部维护的内存空闲块表,并且需要根据一定的算法(例如分配最先找到的不小于申请大小的内存块给请求者,或者分配最适于申请大小的内存块,或者分配最大空闲的内存块等)找到合适大小的空闲内存块。如果该空闲内存块过大,还需要切割成已分配的部分和较小的空闲块。然后系统更新内存空闲块表,完成一次内存分配。类似地,在释放内存时,系统把释放的内存块重新加入到空闲内存块表中。如果有可能的话,可以把相邻的空闲块合并成较大的空闲块。默认的内存管理函数还考虑到多线程的应用,需要在每次分配和释放内存时加锁,同样增加了开销。

可见,如果应用程序频繁地在堆上分配和释放内存,会导致性能的损失。并且会使系统中出现大量的内存碎片,降低内存的利用率。默认的分配和释放内存算法自然也考虑了性能,然而这些内存管理算法的通用版本为了应付更复杂、更广泛的情况,需要做更多的额外工作。而对于某一个具体的应用程序来说,适合自身特定的内存分配释放模式的自定义内存池可以获得更好的性能。

2. 内存池简介

2.1 内存池的定义

池化技术是一种降低频繁操作导致开销过大的方法,如内存池、线程池、进程池和对象池等。

内存池(Memory Pool)是一种内存分配方式。通常我们习惯直接使用new、malloc等API申请内存,这样做的缺点在于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能。

2.2 内存池的实现原理

内存池则是在真正使用内存之前,预先申请分配一定数量、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。

(用malloc申请一大块内存,当要分配的时候,从这一大块内存中一点一点的分配,当一大块内存分配的差不多的时候,再用malloc再申请一大块内存,然后再一点一点的分配给你)

2.3 内存池的优点

减少malloc的次数,减少malloc()调用次数就意味着减少对内存的浪费,使得内存分配效率得到提升。

2.4 内存池的分类

应用程序自定义的内存池根据不同的适用场景又有不同的类型。从线程安全的角度来分,内存池可以分为单线程内存池和多线程内存池。单线程内存池整个生命周期只被一个线程使用,因而不需要考虑互斥访问的问题;多线程内存池有可能被多个线程共享,因此需要在每次分配和释放内存时加锁。相对而言,单线程内存池性能更高,而多线程内存池适用范围更加广泛。

从内存池可分配内存单元大小来分,可以分为固定内存池和可变内存池。所谓固定内存池是指应用程序每次从内存池中分配出来的内存单元大小事先已经确定,是固定不变的;而可变内存池则每次分配的内存单元大小可以按需变化,应用范围更广,而性能比固定内存池要低。

3. 内存池的实现v1.0

3.1 程序源码

通过#define MYMEMPOOL 1,可以使用无内存的申请空间操作。如果注释掉宏定义,将使用普通的申请空间操作。

#include <iostream>
using namespace std;
#include <ctime>

#define MYMEMPOOL 1

class A
{
public:
    static void *operator new(size_t size);
    static void operator delete(void *phead);
    static int m_iCout; //分配计数统计,每new一次,就统计一次
    static int m_iMallocCount; //每malloc一次,就统计一次
private:
    A *next;
    static A* m_FreePosi; //总是指向一块可以分配出去的内存的首地址
    static int m_sTrunkCout; //一次分配多少倍的该类内存
};

int A::m_iCout = 0;
int A::m_iMallocCount = 0;
A *A::m_FreePosi = nullptr;
int A::m_sTrunkCout = 5; //一次分配5倍的该类内存作为内存池子的大小

void *A::operator new(size_t size)
{
  #ifndef MYMEMPOOL
    A *ppoint = (A*)malloc(size);
    return ppoint;
  #endif
    A *tmplink;
    if (m_FreePosi == nullptr)
    {
        //为空,我要申请内存,要申请一大块内存
        size_t realsize = m_sTrunkCout * size; //申请m_sTrunkCout这么多倍的内存
        m_FreePosi = reinterpret_cast<A*>(new char[realsize]); //传统new,调用的系统底层的malloc
        tmplink = m_FreePosi; 

        //把分配出来的这一大块内存(5小块),彼此要链起来,供后续使用
        for (; tmplink != &m_FreePosi[m_sTrunkCout - 1]; ++tmplink)
        {
            tmplink->next = tmplink + 1;
        }
        tmplink->next = nullptr;
        ++m_iMallocCount;
    }
    tmplink = m_FreePosi;
    m_FreePosi = m_FreePosi->next;
    ++m_iCout;
    return tmplink;
}
void A::operator delete(void *phead)
{
  #ifndef MYMEMPOOL
    free(phead);
    return;
  #endif
    (static_cast<A*>(phead))->next = m_FreePosi;
    m_FreePosi = static_cast<A*>(phead);
}

void func()
{
    clock_t start, end; //包含头文件 #include <ctime>
    start = clock();
    //for (int i = 0; i < 500'0000; i++)
    for (int i = 0; i < 15; i++)
    {
        A *pa = new A();
        printf("%p\n", pa);
    }
    end = clock();
    cout << "申请分配内存的次数为:" << A::m_iCout << " 实际malloc的次数为:" << A::m_iMallocCount << " 用时(毫秒): " << end - start << endl;
}
 
int main()
{ 
    func();
    return 1;
}

3.2 实现过程分析

这个C++程序实现了一个简单的内存池(Memory Pool)。内存池是一种用于管理内存分配的数据结构,它通过预先分配大块的内存,然后以较小的单位进行释放,以减少频繁的内存分配和释放导致的开销。

以下是程序的主要步骤和功能:

1.定义了一个名为A的类,该类具有以下成员:

  • operator new和operator delete:这两个成员函数用于分配和释放内存。
  • m_iCout:一个静态成员变量,用于统计new操作的数量。
  • m_iMallocCount:一个静态成员变量,用于统计malloc操作的数量。
  • m_FreePosi:一个静态成员指针,指向一块可以分配出去的内存的首地址。
  • m_sTrunkCout:一个静态成员变量,表示一次要分配多少倍该类内存。

2.在主函数中,调用了func()函数。在func()函数中,执行了以下操作:

  • 记录开始时间。
  • 执行一个循环,循环15次,每次创建一个A类型的对象(通过调用new A())。
  • 记录结束时间。
  • 输出申请分配内存的次数(即new A()的次数)、实际进行malloc的次数以及执行时间。

3.A::operator new:这个成员函数用于分配内存。首先检查是否有可用的内存(即检查m_FreePosi是否为空)。如果为空,则通过调用new char[realsize]来分配一块大小为m_sTrunkCout * size的内存,并将这块内存的首地址转换为A*类型赋值给m_FreePosi。然后,将这块内存分割成若干个小块,并链起来供后续使用。如果已经有可用的内存,则从链表的头部取出一个小块,并更新相关的计数。

4.A::operator delete:这个成员函数用于释放内存。首先将传入的指针的下一个节点设置为m_FreePosi,然后将m_FreePosi更新为传入的指针。

通过以上步骤,程序实现了一个简单的内存池。在程序中,创建和删除对象的操作都通过内存池来进行,减少了频繁的内存分配和释放操作,提高了程序的性能。

3.3 运行结果

可以发现当使用内存池创建15个对象,我们实际上只需要申请三次空间,时间需要82ms

当不使用内存池时,运行结果如下:

通过普通方法创建15个对象,我们需要申请15次空间,但时间需要51ms

总结:单次申请一大块连续的内存相比于每次申请小块内存,内存碎片大大减少,同时减少了malloc的次数,降低了内存的开销(用来监视malloc分配的信息的内存大大减少)。

3.4 不足点

我们通过上面的运行结果可以看到使用内存池虽然分配空间的次数大大减少,但是消耗的时间却变多了。

但随着调用次数的增多,内存池的优势就显现出来了,如下图我们创建500‘000对象

4. 内存池的实现v2.0(嵌入式指针)

4.1 工作原理

借用A对象所占用的内存空间中的前4个字节,这4个字节用来链住这些空闲的内存块;

一旦某一块被分配出去,那么这个块的前4个字节就不再需要,此时这4个字节可以被正常使用;

4.2 使用前提

一般应用在内存池相关的代码中,成功使用嵌入式指针有个前提条件:类A对象的sizeof必须不小于4个字节(这里和前面的四个字节为32位系统中指针的大小;如果64位系统,大小则为8字节)

4.3 嵌入式指针应用举例

class TestEP
  {
  public:
   int m_i;
   int m_j;
 
  public:
   struct obj //结构
   {
     //成员,是个指针
     struct obj *next;  //这个next就是个嵌入式指针
                        //自己是一个obj结构对象,那么把自己这个对象的next指针指向另外一个obj结构对象,
                        //最终,把多个自己这种类型的对象通过链串起来;
 
   };
  };
  void func()
  {
   TestEP mytest;
   cout << sizeof(mytest) << endl; //8
   TestEP::obj *ptemp;  //定义一个指针
   ptemp = (TestEP::obj *)&mytest; //把对象mytest首地址给了这个指针ptemp,这个指针ptemp指向对象mytest首地址;
   cout << sizeof(ptemp->next) << endl; //4
   cout << sizeof(TestEP::obj) << endl; //4
   ptemp->next = nullptr;
 
  }

这里的流程的意思是:将生成的 mytest 对象通过指针转换变成 obj的地址类型, 同时生成一个新的obj指针用来存放它,转换类型以后mytest对象的前半部分则为obj对象,此时则可以调用它的next 对象指向其他的 obj类型地址。

4.4 改进内存池实现(嵌入式指针)

#include <iostream>
using namespace std;
namespace _nmsp4 {
    class myallocator {
    public:
        void *allocate(size_t size) {
            obj *tmplink;
            if (m_FreePosi == nullptr) {
                size_t realsize = m_sTrunkCout * size; //申请m_TrunkCout倍内存
                m_FreePosi = reinterpret_cast<obj *>(malloc(realsize)); //这里的new是系统的new
                tmplink = m_FreePosi;
                //把分配出来的这块内存,彼此要连起来,供后续使用
                for (int i = 0; i< m_sTrunkCout - 1; ++i) {
                    tmplink->next = reinterpret_cast<obj *>(reinterpret_cast<char *>(tmplink) + size);
                    tmplink = tmplink->next;
                }
                tmplink->next = nullptr;
            }
            tmplink = m_FreePosi;
            m_FreePosi = m_FreePosi->next;
            return tmplink;
        }
 
        void deallocate(void *phead) {
            reinterpret_cast<obj *>(phead)->next = m_FreePosi;
            m_FreePosi = reinterpret_cast<obj *>(phead);
        }
 
 
    private:
        struct obj {  
            struct obj *next;  
        };
        obj* m_FreePosi; 
        int m_sTrunkCout = 5;//一次分配多少该类内存
    };
 
    class A {
    public:
        int m_i;
        int m_j;
        static myallocator myalloc;
        static void *operator new(size_t size) {
            return myalloc.allocate(size);
        }
 
        static void operator delete(void *phead) {
            myalloc.deallocate(phead);
        }
    };
 
    myallocator A::myalloc;
 
    void func() {
        A *mypa[100];
        for (int i = 0; i < 15; ++i) {
            mypa[i] = new A();
            printf("%p\n", mypa[i]);
        }
    }
}
 
int main()
{
    _nmsp4::func();
    return 0;
}

运行结果

嵌入式指针可参考:

C++日记——Day52:嵌入式指针概念、内存池改进版

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

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

相关文章

纯css制作常见的图形

1.正方形 <div class"square"></div> .square {width: 100px;height: 100px;background-color: #ffff00;} 效果&#xff1a; 2.长方形 <div class"rectangle"></div> .rectangle{width: 200px;height: 100px;background-color:…

用huggingface.Accelerate进行分布式训练

诸神缄默不语-个人CSDN博文目录 本文属于huggingface.transformers全部文档学习笔记博文的一部分。 全文链接&#xff1a;huggingface transformers包 文档学习笔记&#xff08;持续更新ing…&#xff09; 本部分网址&#xff1a;https://huggingface.co/docs/transformers/m…

Layui快速入门之第一节Layui的基本使用

目录 一&#xff1a;Layui的基本概念 二&#xff1a;Layui使用的基本步骤 1.在官网下载layui的基本文件&#xff0c;引入css和js文件 ①&#xff1a;普通方式引入 ②&#xff1a;第三方 CDN 方式引入 2.在script标签体中编写代码 3.测试 一&#xff1a;Layui的基本概念 …

Mac m1 安装rabbitmq+php-amqplib

rabbitmq 官方地址 https://www.rabbitmq.com mac 软件包 Downloading and Installing RabbitMQ — RabbitMQ 一.这里我选择 homebrew brew updatebrew install rabbitmq二.php代码 用composer 安装 10年软件开发经验,结交朋友! 分销商城系统开发,App商城开发 商务合作 s…

eclipse进入断点之后,一直卡死,线程一直在运行【记录一种情况】

问题描述: 一直卡死在某个断点处&#xff0c;取消断点也是卡死在这边的进程处。 解决方式&#xff1a; 将JDK的使用内存进行了修改 ① 打开eclipse&#xff0c;window->preference->Java->Installed JREs&#xff0c;选中使用的jdk然后点击右侧的edit&#xff0c;在…

打造基于终端命令行的IDE,Termux配置Vim C++开发环境

Termux配置Vim C开发环境&#xff0c;打造基于终端命令行的IDE 主要利用VimCoc插件&#xff0c;配置C的代码提示等功能。 Termux换源 打开termux&#xff0c;输入termux-change-repo 找到mirrors.tuna.tsinghua.edu.cn&#xff0c;清华源&#xff0c;空格选中&#xff0c;回…

LeetCode(力扣)40. 组合总和 IIPython

LeetCode40. 组合总和 II 题目链接代码 题目链接 https://leetcode.cn/problems/combination-sum-ii/ 代码 class Solution:def backtrackingz(self, candidates, target, result, total, path, startindex):if target total:result.append(path[:])return for i in range…

elasticsearch访问9200端口 提示需要登陆

项目场景&#xff1a; 提示&#xff1a;这里简述项目相关背景&#xff1a; elasticsearch访问9200端口 提示需要登陆 问题描述 提示&#xff1a;这里描述项目中遇到的问题&#xff1a; 在E:\elasticsearch-8.9.1-windows-x86_64\elasticsearch-8.9.1\bin目录下输入命令 ela…

手写Spring:第5章-注入属性和依赖对象

文章目录 一、目标&#xff1a;注入属性和依赖对象二、设计&#xff1a;注入属性和依赖对象三、实现&#xff1a;注入属性和依赖对象3.0 引入依赖3.1 工程结构3.2 注入属性和依赖对象类图3.3 定义属性值和属性集合3.3.1 定义属性值3.3.2 定义属性集合 3.4 Bean定义补全3.5 Bean…

Flutter实用工具Indexer列表索引和Search搜索帮助。

1.列表索引 效果图&#xff1a; indexer.dart import package:json_annotation/json_annotation.dart;abstract class Indexer {///用于排序的字母JsonKey(includeFromJson: false, includeToJson: false)String? sortLetter;///用于排序的拼音JsonKey(includeFromJson: fal…

学习笔记|计数器|Keil软件中 0xFD问题|I/O口配置|STC32G单片机视频开发教程(冲哥)|第十二集:计数器的作用和意义

文章目录 1.计数器的用途2.计数器的配置官方例程开始Tips&#xff1a;编译时提示错误FILE DOES NOT EXIST&#xff1a; 3.计数器的应用本例完整代码&#xff1a;总结课后练习&#xff1a; 1.计数器的用途 直流有刷的电机,后面两个一正一负的电接上,电机就可以转 到底是转子个…

NLP(六十八)使用Optimum进行模型量化

本文将会介绍如何使用HuggingFace的Optimum&#xff0c;来对微调后的BERT模型进行量化&#xff08;Quantization&#xff09;。   在文章NLP&#xff08;六十七&#xff09;BERT模型训练后动态量化&#xff08;PTDQ&#xff09;中&#xff0c;我们使用PyTorch自带的PTDQ&…

李宏毅-机器学习hw4-self-attention结构-辨别600个speaker的身份

一、慢慢分析学习pytorch中的各个模块的参数含义、使用方法、功能&#xff1a; 1.encoder编码器中的nhead参数&#xff1a; self.encoder_layer nn.TransformerEncoderLayer( d_modeld_model, dim_feedforward256, nhead2) 所以说&#xff0c;这个nhead的意思&#xff0c;就…

使用Maven创建父子工程

&#x1f4da;目录 创建父工程创建子模块创建子模块示例创建认证模块(auth) 结束 创建父工程 选择空项目&#xff1a; 设置&#xff1a;项目名称&#xff0c;组件名称&#xff0c;版本号等 创建完成后的工程 因为我们需要设置这个工程为父工程所以不需要src下的所有文件 在pom…

WPF Flyout风格动画消息弹出消息提示框

WPF Flyout风格动画消息弹出消息提示框 效果如图&#xff1a; XAML: <Window x:Class"你的名称控件.FlyoutNotication"xmlns"http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x"http://schemas.microsoft.com/winfx/2006/xam…

java八股文面试[数据库]——索引覆盖

覆盖索引是一种避免回表查询的优化策略: 只需要在一棵索引树上就能获取SQL所需的所有列数据&#xff0c;无需回表&#xff0c;速度更快。 具体的实现方式: 将被查询的字段建立普通索引或者联合索引&#xff0c;这样的话就可以直接返回索引中的的数据&#xff0c;不需要再通过聚…

肖sir__设计测试用例方法之因果图07_(黑盒测试)

设计测试用例方法之因果图 一、定义&#xff1a;因果图提供了一个把规格转化为判定表的系统化方法&#xff0c;从该图中可以产生测试数据。其 中&#xff0c;原因是表示输入条件&#xff0c;结果是对输入执 行的一系列计算后得到的输出。 二、因果图方法最终生成的就是判定表。…

rhcsa4 进程和SSH

tree命令。用于以树状结构显示目录和文件。通过运行 “tree” 命令可视化地查看文件系统中的目录结构。 tree / systemd是第一个系统进程&#xff08;pid1&#xff09;不启动&#xff0c;其他进程也没法启动&#xff0c; 用pstree查看进程树 我们可以看到所有进程都是syste…

蓝桥杯打卡Day3

文章目录 吃糖果递推数列 一、吃糖果IO链接 本题思路:本题题意就是斐波那契数列&#xff01; #include <bits/stdc.h>typedef uint64_t i64;i64 f(i64 n) {if(n1) return 1;if(n2) return 2;return f(n-1)f(n-2); }signed main() {std::ios::sync_with_stdio(false);s…

GRU门控循环单元

GRU 视频链接 https://www.bilibili.com/video/BV1Pk4y177Xg?p23&spm_id_frompageDriver&vd_source3b42b36e44d271f58e90f86679d77db7Zt—更新门 Rt—重置门 控制保存之前一层信息多&#xff0c;还是保留当前神经元得到的隐藏层的信息多。 Bi-GRU GRU比LSTM参数少 …