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

这篇具有很好参考价值的文章主要介绍了C++内存管理(3)——内存池。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

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

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

C++内存管理(3)——内存池,C/C++内存管理精讲,c++,嵌入式,内存管理,内存池

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

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 运行结果

C++内存管理(3)——内存池,C/C++内存管理精讲,c++,嵌入式,内存管理,内存池

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

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

C++内存管理(3)——内存池,C/C++内存管理精讲,c++,嵌入式,内存管理,内存池

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

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

3.4 不足点

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

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

C++内存管理(3)——内存池,C/C++内存管理精讲,c++,嵌入式,内存管理,内存池

C++内存管理(3)——内存池,C/C++内存管理精讲,c++,嵌入式,内存管理,内存池

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++内存管理(3)——内存池,C/C++内存管理精讲,c++,嵌入式,内存管理,内存池

嵌入式指针可参考:

C++日记——Day52:嵌入式指针概念、内存池改进版文章来源地址https://www.toymoban.com/news/detail-697787.html

到了这里,关于C++内存管理(3)——内存池的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 嵌入式开发——DMA外设到内存

    加强理解DMA数据传输过程 加强掌握DMA的初始化流程 掌握DMA数据表查询 理解源和目标的配置 理解数据传输特点 能够动态配置源数据 需求 实现串口的数据接收,要求采用dma的方式。 数据交互流程 CPU配置好DMA 外部数据发送给串口外设 串口外设触发中断 CPU处理中断逻辑,通知

    2024年02月03日
    浏览(54)
  • 【小黑嵌入式系统第十五课】μC/OS-III程序设计基础(四)——消息队列(工作方式&数据通信&生产者消费者模型)、动态内存管理、定时器管理

    上一课: 【小黑嵌入式系统第十四课】μC/OS-III程序设计基础(三)——信号量(任务同步资源同步)、事件标记组(与或多个任务) 下一课: 【小黑嵌入式系统第十六课】PSoC 5LP第三个实验——μC/OS-III 综合实验 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣

    2024年01月17日
    浏览(50)
  • C语言嵌入式系统编程注意事项之内存操作

    在嵌入式系统的编程中,常常要求在特定的内存单元读写内容,汇编有对应的MOV指令,而除C/C++以外的其它编程语言基本没有直接访问绝对地址的能力 数据指针 在嵌入式系统的编程中,常常要求在特定的内存单元读写内容,汇编有对应的MOV指令,而除C/C++以外的其它编程语言

    2024年02月09日
    浏览(67)
  • faac内存开销较大,为方便嵌入式设备使用进行优化(valgrind使用)

    faac内存开销较大,为方便嵌入式设备使用进行优化,在github上提了issues但是没人理我,所以就搞一份代码自己玩吧。 基于faac_1_30版本,原工程https://github.com/knik0/faac faac内存优化: faac内存开销较大,为方便嵌入式设备使用进行优化,在github上提了issues但是没人理我,所以就搞

    2024年02月14日
    浏览(46)
  • 【嵌入式——C++】算法(STL)

    需要引入头文件 #include algorithm 遍历容器。 代码示例 搬运容器到另一个容器中。参数1 原容器起始迭代器,参数2 原容器结束迭代器,参数3 目标容器开始迭代器 参数4 函数或者仿函数。 代码示例 查找元素,查找指定元素,找到返回指定元素的迭代器,找不到返回结束迭代器

    2024年02月19日
    浏览(41)
  • 【嵌入式——C++】 数组与函数

    C++ 支持数组数据结构,它可以存储一个固定大小的相同类型元素的顺序集合。数组是用来存储一系列数据,但它往往被认为是一系列相同类型的变量。 声明数组 初始化数组 数组的访问 声明数组 初始化数组 数组的访问 函数是一组一起执行一个任务的语句。每个 C++ 程序都至

    2024年01月18日
    浏览(37)
  • 嵌入式 QT 界面布局管理

    目录 1、实例程序功能 2、界面组件布局 2.1 界面组件的层次关系 2.2 布局管理 2.3 伙伴关系和Tab顺序       创建一个 Widget Application 项目 samp2_2, 在创建窗体时选择基类 QDialog ,生成的类命名为 QWDialog ,并选择生成窗体。     如 此 新建 的项 目 samp2_2 有一 个界 面文 件 qwdi

    2024年02月04日
    浏览(55)
  • 嵌入式:C++ Day7

     源码:

    2024年02月15日
    浏览(35)
  • 全志F1C200S嵌入式驱动开发(从DDR中截取内存)

    【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】         linux内核起来的时候,不一定所有的内存都是分配给linux使用的。有的时候,我们是希望能够截留一部分内存的。为什么保留这部分内存呢?这里面可以有很多的用途。 比如说,第一,

    2024年02月14日
    浏览(41)
  • C++嵌入式编程:硬件控制与物联网

    C++是一种功能强大的编程语言,被广泛应用于嵌入式系统的开发和物联网(IoT)应用程序的编写。C++具有高性能、灵活性以及强大的硬件控制能力,使其成为嵌入式编程和物联网开发的理想选择。在本文中,我们将讨论C++在硬件控制和物联网应用中的重要性、应用领域以及一

    2024年01月16日
    浏览(60)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包