欢迎光临
我们一直在努力

内存池学习笔记

引言

在 C/C++ 程序中,频繁使用 new / delete 或 malloc / free 动态分配小块内存,会导致两个问题:一是调用系统调用(或运行时库)的开销大;二是产生大量内存碎片,降低性能和内存利用率。内存池(Memory Pool) 预先申请一大块内存,然后由程序自己管理分配和回收,像从池子里取水一样快速获取小块内存。

如果把系统默认的分配器比作“每次打水都要去河里舀”,那么内存池就是 “在家门口挖一口井,随用随取” —— 避免了远距离奔波和碎片化混乱。


前置知识

  • 内存碎片:外部碎片(空闲块之间穿插着已分配块,导致无法合并成大块)和内部碎片(分配块比实际需求大)。

  • 分配器(Allocator):管理内存申请与释放的系统组件。

  • 对象池(Object Pool):一种特定内存池,专门分配固定大小的对象。

  • 块(Block):内存池管理的基本单元。

  • 链表:常用空闲块链表来管理未分配的内存。


  • 核心思想

    内存池的基本想法:

    • 预先从系统申请一大块连续内存(称为 pool)。

    • 将这块内存按固定大小分割成多个小块(chunk),每个小块大小通常等于或稍大于应用程序请求的对象大小。

    • 使用空闲链表(free list)将所有空闲块串起来。

    • 分配时,直接从空闲链表中取出一块(O(1))。

    • 释放时,将块插回空闲链表(O(1))。

    固定大小内存池只适用于分配大小相同的对象。如果需要分配不同大小的对象,可以使用分级内存池(维护多个固定大小的池,如 8, 16, 32, … 字节)。

    更高级的方案:slab 分配器(Linux 内核)、tcmalloc(Google)、jemalloc(Facebook)。


    实现一个简单的固定大小内存池(C++)

    cpp

    #include <iostream>
    #include <cstdlib>
    #include <cassert>

    class MemoryPool {
    public:
    MemoryPool(size_t blockSize, size_t blockCount)
    : blockSize_(blockSize), blockCount_(blockCount) {
    // 分配一整块内存
    pool_ = static_cast<char*>(std::malloc(blockSize * blockCount));
    if (!pool_) throw std::bad_alloc();
    // 初始化空闲链表:每个块的前 sizeof(FreeNode*) 字节存储下一个空闲块的地址
    freeHead_ = reinterpret_cast<FreeNode*>(pool_);
    FreeNode* cur = freeHead_;
    for (size_t i = 1; i < blockCount; ++i) {
    char* nextBlock = pool_ + i * blockSize;
    cur->next = reinterpret_cast<FreeNode*>(nextBlock);
    cur = cur->next;
    }
    cur->next = nullptr;
    }

    ~MemoryPool() {
    std::free(pool_);
    }

    void* allocate() {
    if (!freeHead_) return nullptr; // 池已空
    void* ptr = freeHead_;
    freeHead_ = freeHead_->next;
    return ptr;
    }

    void deallocate(void* ptr) {
    if (!ptr) return;
    // 将释放的块放回空闲链表头部
    FreeNode* node = static_cast<FreeNode*>(ptr);
    node->next = freeHead_;
    freeHead_ = node;
    }

    // 禁止拷贝
    MemoryPool(const MemoryPool&) = delete;
    MemoryPool& operator=(const MemoryPool&) = delete;

    private:
    struct FreeNode {
    FreeNode* next;
    };
    char* pool_;
    FreeNode* freeHead_;
    size_t blockSize_;
    size_t blockCount_;
    };

    // 使用示例
    int main() {
    MemoryPool pool(sizeof(int), 100);
    int* a = (int*)pool.allocate();
    *a = 42;
    std::cout << *a << std::endl;
    pool.deallocate(a);
    return 0;
    }

    要点:

    • 每个空闲块的前几个字节存储指向下一个空闲块的指针(侵入式链表)。

    • 分配和释放都是常数时间。

    • 只适用于固定大小,且不保证线程安全。


    性能与性质

    特性系统默认分配器内存池
    分配速度 较慢(涉及系统调用或复杂算法) 极快(单链表操作)
    释放速度 较慢 极快
    内存碎片 容易产生外部碎片 无外部碎片(但可能有内部碎片)
    适用场景 通用,大小不一 大量小块、大小固定或几种固定大小
    线程安全 通常全局锁 需要额外实现

    性质:

    • 内存池不会将内存归还给操作系统(除非整个池销毁),适合长期运行且内存使用稳定的场景。

    • 可以重载 new / delete 操作符,使类对象自动使用内存池。

    • 对于多线程环境,可以给每个线程独立的本地池(thread-local pool)或使用原子操作保护空闲链表。

    拓展

    • 对象池(Object Pool):与内存池类似,但管理的是已经构造好的对象,分配时返回已存在的对象实例(如数据库连接池、线程池)。

    • 池化技术:连接池、线程池、内存池都是“资源复用”思想的体现。

    • slab 分配器:Linux 内核的 slab 分配器将内存划分为不同大小的 cache,并维护每个对象的构造函数/析构函数。

    • 伙伴系统(Buddy System):另一种内存管理算法,平衡碎片和分配速度。


    总结

    内存池通过预先申请大块内存并自行管理分配,避免了频繁的系统调用和内存碎片,极大提升了频繁分配/释放小块内存的性能。它是高性能服务器、游戏引擎、数据库等系统中的基本组件。掌握内存池的实现原理,不仅能写出更高效的代码,还能深入理解计算机内存管理的底层机制。

    赞(0)
    未经允许不得转载:171主机测评 » 内存池学习笔记
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址