专栏首页C/C++基础内存池介绍与经典内存池的实现

内存池介绍与经典内存池的实现

代码编译运行环境:VS2012+Debug+Win32


1.默认内存管理函数的不足

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

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

默认的内存管理函数还考虑到多线程的应用,需要在每次分配和释放内存时加锁,同样增加了开销。 可见,如果应用程序频繁地在堆上分配和释放内存,则会导致性能的损失。并且会使系统中出现大量的内存碎片,降低内存的利用率。

默认的分配和释放内存算法自然也考虑了性能,然而这些内存管理算法的通用版本为了应付更复杂、更广泛的情况,需要做更多的额外工作。而对于某一个具体的应用程序来说,适合自身特定的内存分配释放模式的自定义内存池则可以获得更好的性能。


2.内存池简介

2.1内存池的定义

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

2.2内存池的优点

内存池则是在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样做的一个显著优点是,使得内存分配效率得到提升。

2.3内存池的分类

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


3. 经典的内存池技术

内存池(Memory Pool)技术因为其对内存管理有着显著的优点,在各大项目中应用广泛,备受推崇。但是,通用的内存管理机制要考虑很多复杂的具体情况,如多线程安全等,难以对算法做有效的优化,所以,在一些特殊场合,实现特定应用环境的内存池在一定程度上能够提高内存管理的效率。

经典的内存池(Memory Pool)技术,是一种用于分配大量大小相同的小对象的技术。通过该技术可以极大加快内存分配/释放过程。既然针对是特定对象的内存池,所以内存池一般设置为类模板,根据不同的对象来进行实例化。

3.1经典内存池的设计

3.1.1经典内存池实现过程

(1)先申请一块连续的内存空间,该段内存空间能够容纳一定数量的对象。

(2)每个对象连同一个指向下一个对象的指针一起构成一个内存节点(Memory Node)。各个空闲的内存节点通过指针来形成一个链表,链表的每一个内存节点都是一块可供分配的内存空间。

(3)某个内存节点一旦分配出去,就将从链表中去除。

(4)一旦释放了某个内存节点的空间,又将该节点重新加入自由内存节点链表。

(5)如果一个内存块的所有内存节点分配完毕,若程序继续申请新的对象空间,则会再次申请一个内存块来容纳新的对象。新申请的内存块会加入内存块链表中。

经典内存池的实现过程大致如上面所述,其形象化的过程如下图所示:

如上图所示,蓝色框表示申请的内存块,里面存放三个可供分配的空闲节点。空闲节点由空闲节点链表管理,如果分配出去,将其从空闲节点链表删除,如果释放,将其重新插入到链表的头部(注意,本文实现的不是插入到链表的尾部,当然也可以插入到尾部,读者可自行实现)。如果内存块中的空闲节点不够用,则重新申请内存块,申请的内存块由内存块链表来管理。

3.1.2经典内存池数据结构设计

按照上面的过程设计,内存池类模板有这样几个成员。

两个常量: 内存块大小:MemBlockSize; 节点大小:ItemSize;

两个指针变量: 内存块链表头指针:pMemBlockHeader; 空闲节点链表头指针:pFreeNodeHeader;

空闲节点结构体:

struct FreeNode{
    FreeNode* pNext;
    char data[ObjectSize];
}

内存块结构体:

Struct MemBlock{
    MemBlock *pNext;
    FreeNode data[NumofObjects];
}

3.2经典内存池的实现

根据以上经典内存池的设计,编码实现如下。

/***mempool.h***/
template<int ObjectSize,int NumofObjects=20> class MemPool{
private:
    const int MemBlockSize; //每个内存块的大小
    const int ItemSize;    //每个内存节点的大小

    //空闲节点结构体
    struct FreeNode{
        FreeNode* pNext;
        char data[ObjectSize];
    };

    //内存块结构体
    struct MemBlock{
        MemBlock *pNext;
        FreeNode data[NumofObjects];
    };

    FreeNode* freeNodeHeader;
    MemBlock* memBlockHeader;

public:
    MemPool():ItemSize(ObjectSize+sizeof(FreeNode*)),MemBlockSize(sizeof(MemBlock*)+NumofObjects*(ObjectSize+sizeof(FreeNode*))){
        freeNodeHeader=NULL;
        memBlockHeader=NULL;
    }

    ~MemPool(){
        MemBlock* ptr;
        while(memBlockHeader){
            ptr=memBlockHeader->pNext;
            delete memBlockHeader;
            memBlockHeader=ptr;
        }
    }
    void* malloc();
    void free(void*);
};

//分配空闲的节点
template<int ObjectSize,int NumofObjects> 
void* MemPool<ObjectSize,NumofObjects>::malloc(){
    if(freeNodeHeader==NULL){      //无空闲节点
        MemBlock* newBlock=new MemBlock;
        newBlock->data[0].pNext=NULL; //设置内存块的第一个节点为空闲节点链表的最后一个
        for(int i=1; i<NumofObjects;++i)
            newBlock->data[i].pNext=&newBlock->data[i-1];
        freeNodeHeader=&newBlock->data[NumofObjects-1];
        newBlock->pNext=memBlockHeader;
    }
    //返回空节点闲链表的第一个节点
    void* freeNode=freeNodeHeader;
    freeNodeHeader=freeNodeHeader->pNext;
    return freeNode;
}

//释放已经分配的节点
template<int ObjectSize,int NumofObjects> 
void MemPool<ObjectSize,NumofObjects>::free(void* p){
    FreeNode* pNode=(FreeNode*)p;
    pNode->pNext=freeNodeHeader;//将释放的节点插入空闲节点头部
    freeNodeHeader=pNode;
}
/***end mempool.h***/

/***main.cpp***/
#include <iostream>
using namespace std;

#include "mempool.h"

class ActualClass{
    static int count;
    int No;

public:
    ActualClass(){
        No=count;
        count++;
    }

    void print(){
        cout<<this<<": ";
        cout<<"the "<<No<<" object"<<endl;
    }

    void* operator new(size_t size);
    void operator delete(void* p);
};

//定义内存池对象
MemPool<sizeof(ActualClass),2> mp;

void* ActualClass::operator new(size_t size){
    return mp.malloc();
}

void ActualClass::operator delete(void* p){
    mp.free(p);
}

int ActualClass::count=0;

int main(){
    ActualClass* p1=NULL;
    p1=new ActualClass;
    p1->print();

    ActualClass* p2=new ActualClass;
    p2->print();
    delete p1;

    p1=new ActualClass;
    p1->print();

    ActualClass* p3=new ActualClass;
    p3->print();

    delete p1;
    delete p2;
    delete p3;

    getchar();
}
/***end main.cpp***/

程序运行结果:

3.3程序分析

阅读以上程序,应注意以下几点。 (1)对一种特定的类对象而言,内存池中内存块的大小是固定的,内存节点的大小也是固定的。内存块在申请之初就被划分为多个内存节点,每个Node的大小为ItemSize(对象的大小加上一个指针的大小)。刚开始,所有的内存节点都是自由的,被串成链表。

(2)指针标量memBlockHeader是用来把所有申请的内存块(Memory Block)串成一个内存块链表,以便通过它可以释放所有申请的内存。freeNodeHeader变量则是把所有自由内存节点(Free Node)串成一个链表。freeNodeHeader为空则表明没有可用的自由内存节点,必须申请新的内存块。

(3)申请空间的过程如下。在自由内存节点链表(freeNodeHeader)非空的情况下,malloc过程只是从链表中取下自由内存节点链表的头一个节点,然后把链表头指针移动到下一个节点上去。否则,意味着需要一个新的内存块(Memory Block)。这个过程需要申请新的内存块切割成多个内存节点,并把它们串起来,内存池技术的主要开销就在这里。

(4)释放对象的过程就是把被释放的内存节点重新插入到内存节点链表的开头。最后被释放的节点就是下一个即将被分配的节点。

(5)内存池技术申请/释放内存的速度很快,其内存分配过程多数情况下复杂度为O(1),主要开销在freeNodeHeader为空时需要生成新的内存块。内存节点释放过程复杂度为O(1)。

(6) 在上面的程序中,指针p1和p2连续两次申请空间,它们代表的地址之间的差值为12,正好为一个内存节点的大小(sizeof(ActualClass)+4)的大小。指针p1所指向的对象被释放后,再次申请空间,得到的地址与刚刚释放的地址正好相同。指针p3多代表的地址与前两个对象的地址相聚很远,原因是第一个内存块中的自由内存节点已经分配完了,p3指向的对象位于第二个内存块中。

(7)以上内存池方案并不完美,比如,只能单个单个申请对象空间,不能申请对象数组,内存池中内存块的个数只能增大不能减少,未考虑多线程安全等问题。现在,已经有很多改进的方案,请读者自行查阅相关资料。


参考文献

[1]http://blog.csdn.net/xushiweizh/article/details/1402967 [2]陈刚.C++高级进阶教程[M].武汉:武汉大学出版社,2008[7.8(P268-P272)]

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • *** glibc detected *** malloc(): memory corruption

    在Linux Server上不好模拟出来:不过若是先malloc,再越界memset,再free此内存块,然后malloc新内存块就会出现类似错误。

    Dabelv
  • 程序内存布局

    C/C++程序为编译后的二进制文件,运行时载入内存,运行时内存分布由代码段、初始化数据段、未初始化数据段、堆和栈构成,如果程序使用了内存映射文件(比如共享库、共...

    Dabelv
  • Linux命令(37)——free命令

    free命令用于显示系统内存使用情况,包括物理内存(Physical Memory)、虚拟内存(Swap Memory)、共享内存(Shared Memory)...

    Dabelv
  • 【编程基础】C语言内存使用的常见问题

    所讨论的“内存”主要指(静态)数据区、堆区和栈区空间。数据区内存在程序编译时分配,该内存的生存期为程序的整个运行期间,如全局变量和static关键字所声明的静态...

    程序员互动联盟
  • Zephyr 内存分配

    int k_mem_pool_alloc(struct k_mem_pool *p, struct k_mem_block *block, size_t si...

    无限之生
  • JVM学习系列学习三

        在Java中,创建的所有引用对象类型,都在堆内存中。堆内存中的数据由GC对其进行管理的。其实堆内存也是GC主要管理的地方。

    凯哥Java
  • liteos内存(三)

    内存管理模块管理系统的内存资源,它是操作系统的核心模块之一。主要包括内存的初始化、分配以及释放。

    233333
  • 一文掌握 Linux 性能分析之内存篇

    这个文件记录着比较详细的内存配置信息,使用 cat /proc/meminfo 查看。

    CloudDeveloper
  • iOS标准库中常用数据结构和算法之内存池

    内存池提供了内存的复用和持久的存储功能。设想一个场景,当你分配了一块大内存并且填写了内容,但是你又不是经常去访问这块内存。这样的内存利用率将不高,而且无法复用。...

    欧阳大哥2013
  • Linux 内存相关问题汇总

    这篇文章是对 Linux 内存相关问题的集合,工作中会有很大的帮助。关注公号的朋友应该知道之前我写过从内核态到用户态 Linux 内存管理相关的基础文章,在阅读...

    刘盼

扫码关注云+社区

领取腾讯云代金券