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

2023-10-27

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:嵌入式指针概念、内存池改进版

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++内存管理(3)——内存池 的相关文章

  • 机器Epsilon精度差异

    我正在尝试计算 C 中双精度数和浮点数的机器 epsilon 值 作为学校作业的一部分 我在 Windows 7 64 位中使用 Cygwin 代码如下 include
  • std::vector 与 std::stack

    有什么区别std vector and std stack 显然 向量可以删除集合中的项目 尽管比列表慢得多 而堆栈被构建为仅后进先出的集合 然而 堆栈对于最终物品操作是否更快 它是链表还是动态重新分配的数组 我找不到关于堆栈的太多信息 但
  • -webkit-box-shadow 与 QtWebKit 模糊?

    当时有什么方法可以实现 webkit box shadow 的工作模糊吗 看完这篇评论错误报告 https bugs webkit org show bug cgi id 23291 我认识到这仍然是一个问题 尽管错误报告被标记为RESOL
  • 用于 FTP 的文件系统观察器

    我怎样才能实现FileSystemWatcherFTP 位置 在 C 中 这个想法是 每当 FTP 位置添加任何内容时 我都希望将其复制到我的本地计算机 任何想法都会有所帮助 这是我之前问题的后续使用 NET 进行选择性 FTP 下载 ht
  • 需要帮助优化算法 - 两百万以下所有素数的总和

    我正在尝试做一个欧拉计划 http projecteuler net问题 我正在寻找 2 000 000 以下所有素数的总和 这就是我所拥有的 int main int argc char argv unsigned long int su
  • 重载 (c)begin/(c)end

    我试图超载 c begin c end类的函数 以便能够调用 C 11 基于范围的 for 循环 它在大多数情况下都有效 但我无法理解和解决其中一个问题 for auto const point fProjectData gt getPoi
  • ASP.NET Core 3.1登录后如何获取用户信息

    我试图在登录 ASP NET Core 3 1 后获取用户信息 如姓名 电子邮件 id 等信息 这是我在登录操作中的代码 var claims new List
  • 使用 C# 中的 CsvHelper 将不同文化的 csv 解析为十进制

    C 中 CsvHelper 解析小数的问题 我创建了一个从 byte 而不是文件获取 csv 文件的类 并且它工作正常 public static List
  • 如何获取 EF 中与组合(键/值)列表匹配的记录?

    我有一个数据库表 其中包含每个用户 年份组合的记录 如何使用 EF 和用户 ID 年份组合列表从数据库获取数据 组合示例 UserId Year 1 2015 1 2016 1 2018 12 2016 12 2019 3 2015 91
  • 两个静态变量同名(两个不同的文件),并在任何其他文件中 extern 其中一个

    在一个文件中将变量声明为 static 并在另一个文件中进行 extern 声明 我认为这会在链接时出现错误 因为 extern 变量不会在任何对象中看到 因为在其他文件中声明的变量带有限定符 static 但不知何故 链接器 瑞萨 没有显
  • WcfSvcHost 的跨域异常

    对于另一个跨域问题 我深表歉意 我一整天都在与这个问题作斗争 现在已经到了沸腾的地步 我有一个 Silverlight 应用程序项目 SLApp1 一个用于托管 Silverlight SLApp1 Web 的 Web 项目和 WCF 项目
  • C 编程:带有数组的函数

    我正在尝试编写一个函数 该函数查找行为 4 列为 4 的二维数组中的最大值 其中二维数组填充有用户输入 我知道我的主要错误是函数中的数组 但我不确定它是什么 如果有人能够找到我出错的地方而不是编写新代码 我将不胜感激 除非我刚去南方 我的尝
  • LINQ:使用 INNER JOIN、Group 和 SUM

    我正在尝试使用 LINQ 执行以下 SQL 最接近的是执行交叉联接和总和计算 我知道必须有更好的方法来编写它 所以我向堆栈团队寻求帮助 SELECT T1 Column1 T1 Column2 SUM T3 Column1 AS Amoun
  • 如何在当前 Visual Studio 主机内的 Visual Studio 扩展中调试使用 Roslyn 编译的代码?

    我有一个 Visual Studio 扩展 它使用 Roslyn 获取当前打开的解决方案中的项目 编译它并从中运行方法 程序员可以修改该项目 我已从当前 VisualStudioWorkspace 成功编译了 Visual Studio 扩
  • C# 动态/expando 对象的深度/嵌套/递归合并

    我需要在 C 中 合并 2 个动态对象 我在 stackexchange 上找到的所有内容仅涵盖非递归合并 但我正在寻找能够进行递归或深度合并的东西 非常类似于jQuery 的 extend obj1 obj2 http api jquer
  • 为什么 isnormal() 说一个值是正常的,而实际上不是?

    include
  • C++ 继承的内存布局

    如果我有两个类 一个类继承另一个类 并且子类仅包含函数 那么这两个类的内存布局是否相同 e g class Base int a b c class Derived public Base only functions 我读过编译器无法对数
  • 对于某些 PDF 文件,LoadIFilter() 返回 -2147467259

    我正在尝试使用 Adob e IFilter 搜索 PDF 文件 我的代码是用 C 编写的 我使用 p invoke 来获取 IFilter 的实例 DllImport query dll SetLastError true CharSet
  • C# 中最小化字符串长度

    我想减少字符串的长度 喜欢 这串 string foo Lorem ipsum dolor sit amet consectetur adipiscing elit Aenean in vehicula nulla Phasellus li
  • 指针和内存范围

    我已经用 C 语言编程有一段时间了 但对 C 语言还是很陌生 有时我对 C 处理内存的方式感到困惑 考虑以下有效的 C 代码片段 const char string void where is this pointer variable l

随机推荐

  • Python request-html cv2获取网络图片【canvas base64图片】

    测试网站 http www porters vip captcha clicks html import cv2 import base64 import numpy as np import nest asyncio nest async
  • 电子设计大赛需要具备的知识

    具体的说 有 一 基础知识1 电路原理2 数字电路3 模拟电路 重点 4 元器件的简介二 软件方面 总体编程能力 1 单片机基础与编程 重点 单片机内部结构与工作原理 单片机接口电路 单片机程序设计 单片机开发系统 51系列或AVR单片机
  • 【转】Stephen Wolfram写的乔布斯的回忆录

    无意间在微博上看到Stephen Wolfram也写了回忆Jobs的博客 感觉这个人的名字是相当熟悉 后来看到Mathematica这个软件的名字时就感到非常亲切了 这款软件是以前用过的一款非常强大的数学工具软件 可以解决公式计算 解方程组
  • 产品命名规则(自用)

    产品命名规则 自用 产品id命名规则 共8 型号 3 relay类型 1 计量计类型 1 最大值 1 阶段 1 注 型号 根据产品形态定义 如smartplus 可以定义成sp1 sp是smartplus缩写 1是序号 如果有相同类型 sp
  • 在STM32上创建一个自己的操作系统

    参考文章 http www cnblogs com ansersion p 4328800 html 上面是我的微信和QQ群 欢迎新朋友的加入 之前看了蛮多帖子 不过苦于自己对着基本上是门外汉 基本上只明白个大概 幸亏找到一个分享源码的帖子
  • 阅读resyschina推荐引擎文章感受一

    1 推荐目的在于帮助用户做决策 买到更合适的东西 而促销的目的在于销售商品 2 推荐帮助用户找到感兴趣但是没有想到的东西serendipity 惊喜 3 首页上位置对系统的结果有重大影响 4 推荐系统和搜索的区别在于 推荐系统不需要用户进行
  • YOLO项目服务器配置及云硬盘挂载问题

    资源包配置 首先便是conda虚拟环境创建了 这里我们便不一一赘述了 大家可以参考博主先前的文章 然后就是pytorch的安装了 这里可以使用conda命令或者是pip命令 首先是conda命令 博主在第一个服务器时的安装方式就是这个 很正
  • 关于硬件问题造成的MCU死机,过来人简单的谈一谈

    关于MCU死机问题 近期小编在出差期间遇到多起 且原因不同 所以 今日小白借此机会讲一讲因硬件问题造成的MCU死机 MCU不良 在遇到死机问题时 已经可以判定是硬件原因造成的前提下 大多人的选择是交叉验证MCU 先判定是否是MCU单体不良造
  • 软考-嵌入式系统设计师-笔记:嵌入式系统的项目开发与维护

    文章目录 系统开发过程及其项目管理 过程模型 过程评估 软件能力成熟度模型 CMM 能力成熟度模型集成 CMMI 工具与环境 ISO ICE 25010系统和软件质量模型 系统分析知识 系统设计知识 系统设计概述 结构化设计 面向对象设计
  • 接口的静态方法

    静态接口方法 从java开始 接口当中允许使用静态方法 public static 返回值类型 方法名称 参数列表 方法体 提示 就是将abstract或者default换成static即可 带上方法体 方法样式 public interf
  • 老版本的 mybatis-generator 使用示例

    文章目录 main 入口 generatorConfig xml log4j properties main 入口 import java io File import java util ArrayList import java uti
  • c++返回数组引用的函数(4种方法)

    分享返回数组引用的4种方法 普通法 类型别名 尾置返回类型 decltype include
  • CSS层叠上下文

    在学习z index属性的时候 限制了能够使用z index属性的元素 那么为什么有的元素能够使用z index 因为他创建了一个层叠上下文 对于这个词的理解首先要理解上下文 上下文这个名词的出现有很多地方 块级格式上下文 执行上下文 在不
  • 【转载】VC常用小技巧(2)

    项目 如何干净的删除一个类 1 先删除项目中对应的 h和 cpp文件 选中后用 Delete键删除 2 保存后退出项目 到文件夹中删除实际的 h和 cpp文件 3 删除 clw文件 4 重新进入项目 进行全部重建 rebuild all 如
  • 批处理常用命令及用法

    2019独角兽企业重金招聘Python工程师标准 gt gt gt 批处理常用命令及用法大全 阅读本文需要一定的dos基础概念 象 盘符 文件 目录 文件夹 子目录 根目录 当前目录每个命令的完整说明请加 参数参考微软的帮助文档可以看到 在
  • 前端 - 实习两个星期总结

    文章目录 吐槽 总结 新人建议 项目学习到的 今天已经是菜鸟实习的第二个星期了 怎么说呢 反正就是进的一个不大不小的厂 做着不难不易的事 菜鸟现在主要做的就是适配 现在就来总结一下 不过这之前 菜鸟不得不吐槽一波 吐槽 1 菜鸟进的是一家国
  • kvm常见故障及解决

    一 启动虚拟机Connection reset by peer virsh start vmhost1error Failed to start domain vmhost1error Unable to read from monitor
  • 浙江大学计算机学院最权威的老师,浙江大学计算机科学与技术专业导师介绍:郑能干...

    姓名 郑能干 性别 男 职称 副教授 在岗性质 全职在岗 学院 系 计算机科学与技术学院 招生资格类别 硕士生导师 研究方向 人工智能嵌入式系统神经信息工程普适计算 Email zng cs zju edu cn 个人简介 郑能干 男 19
  • 【计算机视觉】深度学习框架-Keras

    文章目录 1 从零开始训练网络 1 1 搭建网络基本架构 1 2 构建训练网络 1 3 启动训练网络并测试数据 2 用Keras实现一个简单神经网络 2 1 Keras简介 2 2 MNIST手写数字识别 详细解释步骤 2 2 1 数据的加
  • C++内存管理(3)——内存池

    1 默认内存管理函数的不足 为什么使用内存池 利用默认的内存管理操作符 new delete 和函数 malloc free 在堆上分配和释放内存会有一些额外的开销 系统在接收到分配一定大小内存的请求时 首先查找内部维护的内存空闲块表 并且