MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++ 内存池Memory Pool实践

2022-02-251.8k 阅读

什么是内存池

在 C++ 编程中,动态内存分配是一项常用的操作,使用 newdelete 关键字来分配和释放内存。然而,频繁的内存分配和释放会带来一些性能问题。每次调用 newdelete 时,都会涉及系统调用,这会增加开销,降低程序的运行效率。此外,频繁的内存分配和释放还可能导致内存碎片化,使得后续的内存分配变得困难。

内存池(Memory Pool)是一种内存管理技术,旨在解决这些问题。它预先分配一块较大的内存空间,称为内存池。当程序需要分配内存时,直接从内存池中获取小块内存,而不是向操作系统申请新的内存。当内存使用完毕后,将其归还给内存池,而不是释放回操作系统。这样可以减少系统调用的次数,提高内存分配和释放的效率,并且减少内存碎片化的问题。

简单内存池的实现思路

  1. 初始化内存池:在程序开始时,分配一块较大的连续内存空间作为内存池。
  2. 内存分配:当程序请求分配内存时,从内存池中寻找一块合适大小的空闲内存块返回。
  3. 内存释放:当程序释放内存时,将其标记为空闲,以便后续再次分配。

简单内存池的代码示例

#include <iostream>
#include <vector>

class MemoryPool {
private:
    // 内存池大小
    static const size_t poolSize = 1024 * 1024;
    // 内存池指针
    char* pool;
    // 空闲内存块链表头指针
    char* freeList;
    // 已分配内存块大小
    size_t allocatedSize;

public:
    MemoryPool() : pool(nullptr), freeList(nullptr), allocatedSize(0) {
        // 分配内存池
        pool = new char[poolSize];
        // 初始化空闲链表
        freeList = pool;
        for (size_t i = 0; i < poolSize - sizeof(size_t); i += sizeof(size_t)) {
            *reinterpret_cast<size_t*>(reinterpret_cast<char*>(pool + i)) = i + sizeof(size_t);
        }
        *reinterpret_cast<size_t*>(reinterpret_cast<char*>(pool + poolSize - sizeof(size_t))) = 0;
    }

    ~MemoryPool() {
        delete[] pool;
    }

    void* allocate(size_t size) {
        if (size > poolSize) {
            std::cerr << "Request size exceeds pool size." << std::endl;
            return nullptr;
        }
        if (freeList == nullptr) {
            std::cerr << "Memory pool is exhausted." << std::endl;
            return nullptr;
        }
        char* current = freeList;
        freeList = *reinterpret_cast<char**>(freeList);
        allocatedSize += size;
        return current;
    }

    void deallocate(void* ptr) {
        if (ptr < pool || ptr >= pool + poolSize) {
            std::cerr << "Invalid pointer to deallocate." << std::endl;
            return;
        }
        *reinterpret_cast<char**>(ptr) = freeList;
        freeList = static_cast<char*>(ptr);
        allocatedSize -= sizeof(size_t);
    }
};

int main() {
    MemoryPool pool;
    std::vector<void*> pointers;
    for (int i = 0; i < 100; ++i) {
        void* ptr = pool.allocate(100);
        if (ptr) {
            pointers.push_back(ptr);
        }
    }
    for (void* ptr : pointers) {
        pool.deallocate(ptr);
    }
    return 0;
}

在上述代码中,MemoryPool 类实现了一个简单的内存池。pool 是内存池的起始地址,freeList 是空闲内存块链表的头指针。在构造函数中,初始化内存池并构建空闲链表。allocate 方法从空闲链表中取出一块内存块并返回,deallocate 方法将释放的内存块重新加入空闲链表。

内存池的优化

  1. 多粒度内存池:上述简单内存池对于不同大小的内存请求都使用相同的内存块大小,这可能会导致内存浪费。多粒度内存池针对不同大小范围的内存请求,使用不同大小的内存块。例如,对于较小的内存请求,使用较小的内存块;对于较大的内存请求,使用较大的内存块。这样可以提高内存利用率。
  2. 线程安全:在多线程环境下,简单内存池的实现可能会出现竞争条件。为了保证线程安全,可以使用互斥锁(Mutex)来保护内存池的分配和释放操作。当一个线程访问内存池时,先获取互斥锁,操作完成后再释放互斥锁。
  3. 内存回收策略:可以实现更复杂的内存回收策略,例如在内存池使用率较低时,将部分内存归还给操作系统,以减少程序占用的内存空间。

多粒度内存池的实现思路

  1. 划分内存池:将内存池划分为多个子内存池,每个子内存池负责特定大小范围的内存分配。
  2. 映射关系:建立内存请求大小与子内存池的映射关系,以便快速找到合适的子内存池进行内存分配。
  3. 管理子内存池:每个子内存池独立管理自己的空闲内存块链表。

多粒度内存池的代码示例

#include <iostream>
#include <vector>
#include <mutex>

class MemoryPool {
private:
    // 子内存池数量
    static const size_t subPoolCount = 10;
    // 每个子内存池大小
    static const size_t subPoolSize = 1024 * 1024;
    // 子内存池指针数组
    char* subPools[subPoolCount];
    // 空闲内存块链表头指针数组
    char* freeLists[subPoolCount];
    // 已分配内存块大小数组
    size_t allocatedSizes[subPoolCount];
    // 互斥锁数组
    std::mutex mutexes[subPoolCount];

    // 根据请求大小获取子内存池索引
    size_t getSubPoolIndex(size_t size) {
        return size / 128;
    }

public:
    MemoryPool() {
        for (size_t i = 0; i < subPoolCount; ++i) {
            subPools[i] = new char[subPoolSize];
            freeLists[i] = subPools[i];
            for (size_t j = 0; j < subPoolSize - sizeof(size_t); j += sizeof(size_t)) {
                *reinterpret_cast<size_t*>(reinterpret_cast<char*>(subPools[i] + j)) = j + sizeof(size_t);
            }
            *reinterpret_cast<size_t*>(reinterpret_cast<char*>(subPools[i] + subPoolSize - sizeof(size_t))) = 0;
            allocatedSizes[i] = 0;
        }
    }

    ~MemoryPool() {
        for (size_t i = 0; i < subPoolCount; ++i) {
            delete[] subPools[i];
        }
    }

    void* allocate(size_t size) {
        size_t index = getSubPoolIndex(size);
        if (index >= subPoolCount) {
            std::cerr << "Request size exceeds pool capacity." << std::endl;
            return nullptr;
        }
        std::lock_guard<std::mutex> lock(mutexes[index]);
        if (freeLists[index] == nullptr) {
            std::cerr << "Sub - memory pool is exhausted." << std::endl;
            return nullptr;
        }
        char* current = freeLists[index];
        freeLists[index] = *reinterpret_cast<char**>(freeLists[index]);
        allocatedSizes[index] += size;
        return current;
    }

    void deallocate(void* ptr) {
        for (size_t i = 0; i < subPoolCount; ++i) {
            if (ptr >= subPools[i] && ptr < subPools[i] + subPoolSize) {
                std::lock_guard<std::mutex> lock(mutexes[i]);
                *reinterpret_cast<char**>(ptr) = freeLists[i];
                freeLists[i] = static_cast<char*>(ptr);
                allocatedSizes[i] -= sizeof(size_t);
                return;
            }
        }
        std::cerr << "Invalid pointer to deallocate." << std::endl;
    }
};

int main() {
    MemoryPool pool;
    std::vector<void*> pointers;
    for (int i = 0; i < 100; ++i) {
        void* ptr = pool.allocate(100);
        if (ptr) {
            pointers.push_back(ptr);
        }
    }
    for (void* ptr : pointers) {
        pool.deallocate(ptr);
    }
    return 0;
}

在上述代码中,MemoryPool 类实现了一个多粒度内存池。subPools 数组存储各个子内存池的起始地址,freeLists 数组存储各个子内存池的空闲链表头指针,mutexes 数组用于保证线程安全。getSubPoolIndex 方法根据请求大小计算对应的子内存池索引。allocatedeallocate 方法在操作子内存池时,先获取对应的互斥锁。

内存池在实际项目中的应用场景

  1. 游戏开发:在游戏中,经常需要频繁地创建和销毁对象,如游戏角色、子弹等。使用内存池可以显著提高内存分配和释放的效率,减少卡顿现象,提升游戏的流畅度。
  2. 网络编程:在网络服务器中,处理大量的网络请求时,需要频繁地分配和释放内存来存储请求数据和响应数据。内存池可以避免频繁的系统调用,提高服务器的性能和并发处理能力。
  3. 数据库系统:数据库系统需要管理大量的数据结构,如索引、记录等。内存池可以有效地管理这些数据结构的内存分配,提高数据库的读写性能。

内存池的局限性

  1. 内存占用:内存池需要预先分配一定大小的内存空间,即使在程序初期可能并不需要这么多内存,这可能会导致内存浪费。如果内存池分配过大,会占用过多系统资源;如果分配过小,又可能无法满足程序的内存需求。
  2. 实现复杂度:实现一个高效、线程安全且具有良好内存利用率的内存池并不容易。需要考虑多粒度内存管理、内存回收策略、线程同步等多个方面,增加了代码的复杂度和维护成本。
  3. 兼容性:某些特殊的内存分配需求,如需要特定的内存对齐方式,可能难以在内存池中实现。此外,不同操作系统和编译器对内存管理的底层实现可能存在差异,这也会影响内存池的兼容性。

总结内存池相关要点

内存池是一种强大的内存管理技术,通过预先分配内存并重复利用,可以有效提高程序的性能和内存利用率。在实际应用中,需要根据具体的场景和需求选择合适的内存池实现方式。简单内存池适用于单线程且内存请求大小较为均匀的场景;多粒度内存池适用于内存请求大小差异较大的场景;而在多线程环境下,需要实现线程安全的内存池。同时,也要清楚认识到内存池的局限性,权衡其带来的性能提升与增加的复杂度和内存占用等问题。通过合理地使用内存池技术,可以使 C++ 程序在内存管理方面更加高效和稳健。

以上就是关于 C++ 内存池实践的详细内容,希望通过这些介绍和代码示例,能帮助你更好地理解和应用内存池技术。在实际项目中,可以根据具体需求对内存池进行进一步的优化和定制。例如,结合具体业务场景调整子内存池的划分粒度,优化内存回收策略以适应程序的内存使用模式等。同时,要注意内存池与其他内存管理机制(如智能指针)的协同使用,确保程序的内存安全性和稳定性。

通过不断地实践和优化,将内存池技术融入到 C++ 程序的开发中,能够有效地提升程序的整体性能,尤其是在对性能要求较高的应用场景下,如高性能服务器、实时渲染等领域,内存池的优势将更加明显。希望读者在掌握基本的内存池实现方法后,能够深入研究并将其应用到实际项目中,为提升程序的性能贡献一份力量。