问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

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

创作时间:
作者:
@小白创作中心

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

引用
CSDN
1.
https://blog.csdn.net/technologyleader/article/details/138562210

在C++编程中,内存管理是一个核心话题。默认的内存管理函数(如new/delete和malloc/free)虽然功能强大,但在频繁分配和释放内存的场景下可能会导致性能损失和内存碎片问题。为了解决这些问题,内存池技术应运而生。本文将详细介绍内存池的概念、优点以及经典内存池的实现方法。

默认内存管理函数的不足

使用默认的内存管理操作符 new/delete 和函数 malloc()/free() 在堆上分配和释放内存会有一些额外的开销。系统在接收到分配一定大小内存的请求时,需要查找内部维护的内存空闲块表,并根据一定的算法找到合适大小的空闲内存块。如果该空闲内存块过大,还需要切割成已分配的部分和较小的空闲块。在释放内存时,系统需要把释放的内存块重新加入到空闲内存块表中。此外,为了支持多线程应用,每次分配和释放内存时都需要加锁,这进一步增加了开销。

如果应用程序频繁地在堆上分配和释放内存,会导致性能的损失,并且会使系统中出现大量的内存碎片,降低内存的利用率。默认的分配和释放内存算法虽然考虑了性能,但为了应付更复杂、更广泛的情况,需要做更多的额外工作。相比之下,适合自身特定内存分配释放模式的自定义内存池可以获得更好的性能。

内存池简介

内存池的定义

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

内存池的优点

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

内存池的分类

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

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

经典的内存池技术

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

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

经典内存池的设计

经典内存池实现过程
  1. 先申请一块连续的内存空间,该段内存空间能够容纳一定数量的对象;
  2. 每个对象连同一个指向下一个对象的指针一起构成一个内存节点(Memory Node)。各个空闲的内存节点通过指针形成一个链表,链表的每一个内存节点都是一块可供分配的内存空间;
  3. 某个内存节点一旦分配出去,从空闲内存节点链表中去除;
  4. 一旦释放了某个内存节点的空间,又将该节点重新加入空闲内存节点链表;
  5. 如果一个内存块的所有内存节点分配完毕,若程序继续申请新的对象空间,则会再次申请一个内存块来容纳新的对象。新申请的内存块会加入内存块链表中。

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

如上图所示,申请的内存块存放三个可供分配的空闲节点。空闲节点由空闲节点链表管理,如果分配出去,将其从空闲节点链表删除,如果释放,将其重新插入到链表的头部。如果内存块中的空闲节点不够用,则重新申请内存块,申请的内存块由内存块链表来管理。

注意,本文涉及到的内存块链表和空闲内存节点链表的插入,为了省去遍历链表查找尾节点,便于操作,新节点的插入均是插入到链表的头部,而非尾部。当然也可以插入到尾部,读者可自行实现。

经典内存池数据结构设计

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

  • 内存块链表头指针:pMemBlockHeader
  • 空闲节点链表头指针:pFreeNodeHeader

空闲节点结构体:

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

内存块结构体:

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

经典内存池的实现

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

#include <iostream>
using namespace std;

template<int ObjectSize, int NumofObjects = 20>
class MemPool {
private:
    // 空闲节点结构体
    struct FreeNode {
        FreeNode* pNext;
        char data[ObjectSize];
    };
    // 内存块结构体
    struct MemBlock {
        MemBlock* pNext;
        FreeNode data[NumofObjects];
    };
    FreeNode* freeNodeHeader;
    MemBlock* memBlockHeader;
public:
    MemPool() {
        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->pNext = NULL;
        freeNodeHeader = &newBlock->data[0]; // 设置内存块的第一个节点为空闲节点链表的首节点
        // 将内存块的其它节点串起来
        for (int i = 1; i < NumofObjects; ++i) {
            newBlock->data[i - 1].pNext = &newBlock->data[i];
        }
        newBlock->data[NumofObjects - 1].pNext = NULL;
        // 首次申请内存块
        if (memBlockHeader == NULL) {
            memBlockHeader = newBlock;
        } else {
            // 将新内存块加入到内存块链表。
            newBlock->pNext = memBlockHeader;
            memBlockHeader = newBlock;
        }
    }
    // 返回空节点闲链表的第一个节点。
    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;
}

class ActualClass {
    static int count;
    int No;
public:
    ActualClass() {
        No = count;
        count++;
    }
    void print() {
        cout << this << ": ";
        cout << "the " << No << "th 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 = 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;
}

程序运行结果:

004AA214: the 0th object
004AA21C: the 1th object
004AA214: the 2th object
004AB1A4: the 3th object

程序分析

阅读以上程序,应注意以下几点:

  1. 对一种特定的类对象而言,内存池中内存块的大小是固定的,内存节点的大小也是固定的。内存块在申请之初就被划分为多个内存节点,每个 Node 的大小为 ItemSize。刚开始,所有的内存节点都是空闲的,被串成链表。
  2. 成员指针变量 memBlockHeader 是用来把所有申请的内存块连接成一个内存块链表,以便通过它可以释放所有申请的内存。freeNodeHeader 变量则是把所有空闲内存节点串成一个链表。freeNodeHeader 为空则表明没有可用的空闲内存节点,必须申请新的内存块。
  3. 申请空间的过程如下。在空闲内存节点链表非空的情况下,malloc 过程只是从链表中取下空闲内存节点链表的头一个节点,然后把链表头指针移动到下一个节点上去。否则,意味着需要一个新的内存块。这个过程需要申请新的内存块切割成多个内存节点,并把它们串起来,内存池技术的主要开销就在这里。
  4. 释放对象的过程就是把被释放的内存节点重新插入到内存节点链表的开头。最后被释放的节点就是下一个即将被分配的节点。
  5. 内存池技术申请/释放内存的速度很快,其内存分配过程多数情况下复杂度为 O(1),主要开销在 freeNodeHeader 为空时需要生成新的内存块。内存节点释放过程复杂度为 O(1)。
  6. 在上面的程序中,指针 p1p2 连续两次申请空间,它们代表的地址之间的差值为 8,正好为一个内存节点的大小(sizeof(FreeNode))。指针 p1 所指向的对象被释放后,再次申请空间,得到的地址与刚刚释放的地址正好相同。指针 p3 多代表的地址与前两个对象的地址相聚很远,原因是第一个内存块中的空闲内存节点已经分配完了,p3 指向的对象位于第二个内存块中。

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

本文原文来自CSDN

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号