高性能内存池实现与设计

内存池

为什么要手动创建内存池?

因为使用系统的内存,在使用new/delete/malloc/free操作时,为了防止内存泄露问题,我们需要new/delete、malloc/free配套使用,而我们不断进行内存申请与释放会增大开销,同时还会导致内存碎片化的问题。关于内存碎片化就好比,虽然剩余内存有这么大,但是没有一个连续的大内存空间,当用户需要申请一块连续的大内存时,导致申请失败。

new(关键字)失败会抛出异常返回std::bad_alloc。malloc(标准库函数)不会抛出异常返回nullptr。

创建内存池的作用

首先创建内存池,我们需要管理内存池。简单来说,我们不延续之前的RALL思想,我们通过实现申请一篇内存,打造为内存池,当用户使用时,只需向内存池申请内存,而不需要向内核申请。因为,内存池已经向内核申请一大块连续的内存,用户只需使用内存池的内存就行。而在使用完内存后,用户需要释放内存,但不是真的释放,将内存还回内存池,达到复用的结果。我们设计内存池后,可以减少频繁的向内核申请内存,减少开销,并减少内存碎片化。但值得注意的是,我们设计的内存池,只面向小内存申请,当需要申请过大内存仍然使用原有操作,因为需要申请过大内存,我们内存池的优势就没有了,内存池优化建立于对象尺寸远小于内存池。

性能优化的角度:

  • 减少动态内存分配的开销
  • 避免内存碎片
  • 降低系统调用频率

确定性:稳定的分配时间。使用内存池可以使分配和释放操作的耗时更加可控稳定,适合实时性有严格要求的系统。

使用场景

服务器开发、高性能计算、实时系统、游戏开发、网络编程、内存管理器等。

缺点

复杂度:比使用常规操作复杂

初始化内存:需要事先分配内存

不适合大型对象

内存池框架

我们是基于开源项目TCMalloc(Thread-Caching Malloc)实现的,该项目是Goole开发的内存分配器。实现了高效的内存池,旨在优化内存分配和释放的性能,特别是在多线程环境下。内存池通过分层缓存架构来管理内存,有三层:

  • ThreadCache 线程本地缓存。每个线程独立的内存缓存,无锁操作,快速分配和释放内存。减少线程间的竞争,提高并发性
  • CentralCache 中心缓存。管理多个线程共享的内存块;通过自旋锁保护,确保线程安全;批量从PageCache获取内存,分配给ThreadCache。
  • PageCache 页内存。从操作系统获取大块内存;将大块内存分成小块,供CentralCache使用;负责内存的回收和再利用。

线程本地化缓存

我们通过array链表,为每个线程创建自由链表,将利用index作为主键分配(根据大小分配链表索引,利用链表指针来分配内存和回收)。当内存不足时向CentralCache申请内存,并根据自定义设计决定是否需要将内存归还给中心缓存。

简单代码如下:

// 线程本地缓存
class ThreadCache
{
public:
    static ThreadCache* getInstance()
    {
        static thread_local ThreadCache instance;
        return &instance;
    }

    void* allocate(size_t size);
    void deallocate(void* ptr, size_t size);
private:
    ThreadCache() 
    {
        // 初始化自由链表和大小统计
        freeList_.fill(nullptr);
        freeListSize_.fill(0);
    }
    
    // 从中心缓存获取内存
    void* fetchFromCentralCache(size_t index);
    // 归还内存到中心缓存
    void returnToCentralCache(void* start, size_t size);

    bool shouldReturnToCentralCache(size_t index);
private:
    // 每个线程的自由链表数组
    std::array<void*, FREE_LIST_SIZE>  freeList_; 
    std::array<size_t, FREE_LIST_SIZE> freeListSize_; // 自由链表大小统计   
};

中心缓存

struct SpanTracker
{
    std::atomic<void*> spanAddr{nullptr};
    std::atomic<size_t> numPages{0};
    std::atomic<size_t> blockCount{0};
    std::atomic<size_t> freeCount{0}; //用于追踪span还有多少块空闲,都空闲,归还span到PageCache
};

class CentralCache
{
public:
    static CentralCache& getInstance()
    {
        static CentralCache instance;
        return instance;
    }
    void* fetchRange(size_t index);
    void returnRange(void* start, size_t size, size_t index);

private:
    //相互是还所有原子指针为 nullptr
    CentralCache();
    //从页缓存获取内存
    void* fetchFromPageCache(size_t size);

    //获取span信息
    SpanTracker* getSpanTracker(void* blockAddr);

    //更新span空闲计数并检查是否可以归还
    void updateSpanFreeCount(SpanTracker* tracker, size_t newFreeBlocks, size_t index);

private:
    //中心化缓存的自由链表
    std::array<std::atomic<void*>, FREE_LIST_SIZE> centralFreeList_;

    //用于同步自旋锁
    std::array<std::atomic_flag, FREE_LIST_SIZE> lock_;

    //使用数组存储span信息,避免map开销
    std::array<SpanTracker, 1024> spanTrackers_;
    std::atomic<size_t> spanCount_{0};

    //延迟归还相关的成员变量
    static const size_t MAX_DELAY_COUNT = 48; //最大延迟计数
    std::array<std::atomic<size_t>, FREE_LIST_SIZE> delayCounts_; //每个大小类的延迟计数
    std::array<std::chrono::steady_clock::time_point, FREE_LIST_SIZE> lastReturnTimes_;
    static const std::chrono::milliseconds DELAY_INTERVAL;  //延迟间隔

    bool shouldPerformDelayedReturn(size_t index, size_t currentCount, std::chrono::steady_clock::time_point currentTime);
    void performDelayedReturn(size_t index);

};

我们向中心化缓存申请空间是按照传入参数index申请的,因为通过index我们能确定空间大小的范围,分配出比较合适的内存,方便对其颗粒度,减少不必要的内存浪费。不会出现我只需要8b的内存,而分配512的内存。而申请超过限定值的需要直接向系统申请内存。

页缓存

public:
    static const size_t PAGE_SIZE = 4096;  //一页4k
    //单例模式
    static PageCache& getInstance()
    {
        static PageCache instance;
        return instance;
    }
    //分配指定页面的Span
    void* allocateSpan(size_t numPages);

    //释放span
    void deallocateSpan(void* ptr, size_t numPages);

private:
    PageCache() = default;
    //向系统申请空间
    void* systemAlloc(size_t numPages);
private:
    struct Span
    {
        void* pageAddr; //页起始地址
        size_t numPages;        //页数量
        Span* next;             //链表指针
    };
    //按页数管理Spans,不同页面对应不同的Spans
    std::map<size_t, Span*> freeSpans_;
    //页号到Span的映射,用于回收
    std::map<void*, Span*> spanMap_;
    std::mutex mutex_;
};

我们页面缓存是以页为单位的4K,可以减少内存碎片,提高内存管理效率,硬件层次优化(操作系统虚拟内存也是以页为单位的),适应不同内存大小的内存需求。

我们回收内存尝试将连续的空闲页面进行合并,防止多次使用后导致碎片化的问题。

项目所设计的工作有:

优化内存管理。

结合定时器实现批量内存管理

高效同步机制。

所涉及的八股文知识有:多线程,原子操作(atomic)、设计模式、自旋锁、互斥锁、高并发、RALL等

八股文补充

死锁是指在多线程或多进程程序中,多个进程或线程在执行过程中,因争夺资源而造成一种相互等待的状态,从而导致程序无法继续执行。

死锁的四个必要条件

死锁的发生必须满足以下四个条件,这四个条件通常被称为“死锁的必要条件”:

  1. 互斥条件:至少有一个资源是处于“非共享”状态的,即每次只能有一个线程或进程使用这个资源。如果有其他线程请求该资源,必须等待。
  2. 占有并等待:至少有一个线程已经持有了一个资源,并且正在等待其他线程所持有的资源。
  3. 不剥夺条件:资源不能被强制从持有者的手中剥夺,只有在持有者自行释放资源时,其他线程才能获得该资源。
  4. 循环等待:存在一种线程或进程的循环等待关系,即线程 A 等待线程 B 持有的资源,线程 B 等待线程 C 持有的资源,最终线程 C 又等待线程 A 持有的资源,形成一个环形等待链。

死锁的危害

  • 系统崩溃或性能下降:死锁会导致程序停滞不前,使得受影响的线程无法继续执行。严重时,整个应用程序的性能会受到影响,甚至导致系统崩溃。
  • 资源浪费:死锁会造成资源的长时间占用,而无法被其他线程或进程使用,导致资源的浪费。
  • 用户体验差:死锁的存在可能导致应用程序的响应延迟或无响应,从而影响用户体验。

死锁的预防策略

  1. 避免互斥条件:如果可能的话,尽量避免将资源设置为“互斥”资源,即允许多个线程或进程同时访问某些资源。这在一些资源的设计上可能实现,如共享内存、并发数据结构等。
  2. 避免占有并等待一次性请求:要求线程在开始时请求所有所需的资源。如果不能同时获得所有资源,则不分配任何资源,从而避免线程持有部分资源时再等待其他资源。
  3. 避免不剥夺条件:如果一个线程请求资源时发现部分资源被其他线程占用,它可以释放自己已经持有的资源并重新尝试请求,避免形成不剥夺条件。
  4. 避免循环等待资源排序法:按固定顺序申请资源,如果一个线程请求的资源没有满足要求,它会按照顺序排队,直到所有资源都可以满足为止。这样可以避免循环等待。例如,a,b两个进程都需要资源1,资源2,如果a进程获得了资源1,然后b进程获得了资源2就会导致死锁,如果强制要求必须先获得资源1再获得资源2,如果某个进程先获得了资源1,那么另一个进程就会等待,直到资源1的释放,就打破死锁了。

死锁的检测和恢复

尽管死锁预防非常重要,但在一些复杂的并发环境中,无法保证完全避免死锁。因此,死锁的检测和恢复也很重要。

死锁检测

死锁检测是指通过某些算法在程序运行时检测是否发生了死锁。常见的死锁检测方法有:

  1. 资源分配图法:资源分配图是一种图结构,其中节点表示资源和进程,边表示资源与进程之间的关系。如果图中存在一个环,则说明发生了死锁。
  2. 等待图法:通过分析线程之间的等待关系,构建一个等待图。如果该图中有环,则表明程序发生了死锁。
死锁恢复

一旦检测到死锁,系统需要采取措施来恢复正常状态。常见的死锁恢复策略有:

  1. 撤销进程:通过终止一个或多个进程来破坏死锁的循环等待关系,进程终止后释放它所占用的资源,从而让其他进程继续执行。
  2. 资源抢占:系统可以通过抢占某个进程的资源并分配给其他进程,直到资源分配满足并避免死锁。
  3. 回滚:将进程回滚到之前的某个安全状态,释放资源并重新尝试获取资源。

项目中存在内存对其的问题:内存对齐主要是为了提高数据访问效率。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值