写点什么

一个 cpp 协程库的前世今生(二十四)对象池与栈内存池

作者:SkyFire
  • 2022 年 1 月 26 日
  • 本文字数:4233 字

    阅读完需:约 14 分钟

一个cpp协程库的前世今生(二十四)对象池与栈内存池

为了防止大家找不到代码,还是先贴一下项目链接:


GitHub - skyfireitdiy/cocpp at cocpp-0.1.0


本文将介绍一些 cocpp 在内存利用上的设计,主要包括对象池与栈内存池。

对象池

原理

对象池的实现是为了一些可能会频繁创建销毁对象的性能提升,节省内存分配的时间。


当一个对象被释放时,可以不释放内存,而是将内存块加入对象池中,这样后面如果又有此类对象的创建,可以复用对象池中的内存,不用重新分配内存。,从而提升对象创建的速度。

接口

对象池的设计位于 include/cocpp/mem/co_object_pool.h,接口如下:


template <typename ObjectType>class co_object_pool final : private co_noncopyable{private:    std::deque<void*> pool__;                                     // 内存池    co_spinlock       mu__ { co_spinlock::lock_type::in_thread }; // 互斥锁    size_t            max_cap__;                                  // 最大容量public:    co_object_pool(size_t max_cap); // 构造函数    template <typename... ConstructParam>    ObjectType* create_obj(ConstructParam&&... params); // 创建对象    void        destroy_obj(ObjectType* obj);           // 销毁对象    void        clear_free_object();                    // 清空空闲对象    ~co_object_pool();                                  // 析构函数};
复制代码


其实现是一个模板类,可以兼容各种不同的对象。


先介绍一下成员函数:


  • co_object_pool:这个函数接受一个参数 max_cap,规定了对象池中保留的最大对象数量,当对象池中的对象数量到达此值时,后续如果有对象销毁,会直接释放内存。

  • create_obj:创建对象,参数为对象构造函数需要接受的参数列表,返回创建的对象指针。

  • destroy_obj:销毁对象,此函数会调用对象的析构函数,而是否释放内存取决于当前对象池中的空闲对象数量是否达到设置的 max_cap 阈值。

  • clear_free_object:此接口将会释放掉池中所有的空闲内存块,以此来向系统归还内存。


接下来是成员变量:


  • pool__:空闲的内存块队列。

  • mu__:用于保护空闲内存块队列。

  • max_cap__:设置的最大空闲块数量阈值,当实际的空闲块数量到达此阈值时,后续的内存释放将直接将内存归还到操作系统。嗯。

实现

相关的实现也位于 include/cocpp/mem/co_object_pool.h:


template <typename ObjectType>co_object_pool<ObjectType>::co_object_pool(size_t max_cap)    : max_cap__(max_cap){}
template <typename ObjectType>template <typename... ConstructParam>ObjectType* co_object_pool<ObjectType>::create_obj(ConstructParam&&... params){ std::lock_guard<co_spinlock> lck(mu__); void* mem = nullptr; if (pool__.empty()) { mem = std::aligned_alloc(alignof(ObjectType), sizeof(ObjectType)); } else { mem = pool__.front(); pool__.pop_front(); } assert(mem != nullptr); return new (mem) ObjectType(std::forward<ConstructParam>(params)...);}
template <typename ObjectType>void co_object_pool<ObjectType>::destroy_obj(ObjectType* obj){ std::lock_guard<co_spinlock> lck(mu__); obj->~ObjectType(); if (pool__.size() > max_cap__) { free(obj); } else { pool__.push_front(obj); // 尽快用到这块内存 }}
template <typename ObjectType>void co_object_pool<ObjectType>::clear_free_object(){ std::lock_guard<co_spinlock> lck(mu__); for (auto& obj : pool__) { free(obj); } pool__.clear();}
template <typename ObjectType>co_object_pool<ObjectType>::~co_object_pool(){ clear_free_object();}
复制代码


整体的实现比较简单,这里需要注意的一点是,在申请内存的时候需要按照数据结构的对齐方式来申请对齐的内存,然后使用 placement new 的方式来创建对象。


在销毁对象的时候直接调用对象的析构函数。当空闲内存块的数量没有到达阈值时,会将要释放的对象所属的内存块添加到队列的头部,申请的时候也会从头部获取,这样就可以尽快使用到刚刚加入的内存,具有更好的时间局部性。


对象池的使用场景比较多,在所有的工厂中都有使用(ctx 工厂、env 工厂、栈工厂)。


另外还有一点需要注意的是,对象池的内存只包括对象自身所占的内存,如果对象的内部有指针指向其他的内存,这一部分内存是不是管理的,深情与释放都是走操作系统的接口。

栈内存池

上面提到了对象池不会管理对象中指向其他位置的内存,一个典型的例子就是栈,其结构如下(include/cocpp/core/co_stack.h):


class co_stack final : private co_noncopyable{    co_byte* stack__;                          // 堆栈指针    size_t   size__;                           // 堆栈大小    co_stack(co_byte* ptr, size_t stack_size); // 构造函数public:    size_t   stack_size() const; // 堆栈大小    co_byte* stack() const;      // 堆栈指针    co_byte* stack_top() const;  // 堆栈顶部指针
friend class co_object_pool<co_stack>;};
复制代码


可以看到其内部的数据,其实大部分都是由 stack__指针指向的在堆上分配的数据,而这一部分内存不受对象池的管理。因此,频繁创建和销毁协程栈仍具有性能的问题。


在此背景下为栈专门设立了栈内存池。

规则

占内存池中的内存按照不同的大小分为数个区域(zone)。每个区域的大小是二倍的关系。比如第 1 个区域的大小是每个内存块 8K。那么第 2 个区域的大小就是每个内存快 16K,因此在创建协程的时候,将协程栈大小指定为 2 的整数次幂所以有着利用内存池空间,避免内部碎片。



如上图所示的是一个 7 个区的栈内存池,每个区管理的内存大小为 N、2N、4N、……、64N。


每个区下面是对应大小内存块的队列,当有内存请求的时候,先根据请求的内存大小计算出内存区,然后在对应内存区下查找空闲内存块。释放时,也是直接释放到对应内存区域的队列中(也有阈值的限制,与对象池一样)。


当申请的内存超过最大区域管理的内存块大小时,将会直接从操作系统申请和释放内存。

接口

栈内存池的接口位于 include/cocpp/mem/co_mem_pool.h:


class co_mem_pool final : private co_noncopyable{private:    static unsigned long long         align_2_zone_edge__(unsigned long long 南市); // 对齐到zone边界    static unsigned long long         align_size__(unsigned long long size);        // 对齐到8字节    size_t                            get_zone_index__(size_t size) const;          // 获取zone的索引    co_spinlock                       mu__ { co_spinlock::lock_type::in_thread };   // 互斥锁    const size_t                      min_zone__;                                   // 最小zone    const size_t                      zone_count__;                                 // zone数量    const size_t                      max_cap__;                                    // 最大容量    std::vector<std::deque<co_byte*>> mem_pool__;                                   // 内存池public:    co_mem_pool(size_t min_zone, size_t zone_count, size_t max_cap); // 构造函数    co_byte* alloc_mem(size_t size);                                 // 分配内存    void     free_mem(co_byte* ptr, size_t size);                    // 释放内存    void     free_pool();                                            // 释放内存池    ~co_mem_pool();                                                  // 析构函数};
复制代码


首先来看一下成员函数:


  • co_mem_pool:构造函数接收三个参数,其含义分别为:最小的区域号,区域数量,每个区域下面空闲内存块的最大数量。最小区域号的意思是,最小的内存并不是从 2 字节开始,而是从一个基础值(比如 64 字节)开始的(此时 min_zone 对应 7)。

  • alloc_mem:分配内存,返回分配的内存指针。

  • free_mem:释放内存参数为内存对应的指针和内存大小。根据内存大小函数会计算其所属的区域,然后将该内存释放到该对应区域的空闲列表中。

  • free_pool:释放内存池中所有的空闲内存。你好。


还有几个私有的成员函数:


  • align_2_zone_edge__:此函数用于寻找可以容纳 size 大小内存的最小区域 index。

  • get_zone_index__:是函数与 align_2_zone_edge__功能类似,在 align_2_zone_edge__的基础上减去了修正值(考虑 min_zone__)。

  • align_size__:将内存对齐到指针大小(栈内存必须以指针的大小对齐)。


接下来看一下成员变量:


  • mu__ :保护内部数据结构的锁。

  • min_zone__、zone_count__、max_cap__:构造函数传入的三个值。

  • mem_pool__:实际的内存保存对象。一共有 zone_count__项,每项最多容纳 max_cap__块内存。

实现

这些成员函数的实现位于 source/cocpp/mem/co_mem_pool.cpp,此处仅重点看一下分配相关的函数。


co_byte* co_mem_pool::alloc_mem(size_t size){    size_t zone_index = get_zone_index__(size);    if (zone_index >= zone_count__)    {        return reinterpret_cast<co_byte*>(std::aligned_alloc(sizeof(void*), align_size__(size)));    }
std::lock_guard<co_spinlock> lock(mu__); if (mem_pool__[zone_index].empty()) { return reinterpret_cast<co_byte*>(std::aligned_alloc(sizeof(void*), 1 << (zone_index + min_zone__))); } auto ret = mem_pool__[zone_index].front(); mem_pool__[zone_index].pop_front(); return ret;}
复制代码


此函数先判断 size 对应的内存区是否超出可管理的大小,如果超出的可管理大小。就直接分配内存(对齐)。


如果是可以管理的内存大小,查看内存区域最硬的列表中还有没有空闲的内存块,如果没有的话直接申请。


如果有送钱的内存块可用将空闲的内存块从对应的内存区中取出返回。


其余的实现与对象池实现类似,可以查看相关代码了解。

总结

本文介绍的 cocpp 中两种管理内存的方式,对象池与栈内存池,简要阐述了其实现原理。若想深入了解,可以参考源码。


内存管理的实现只是最简单的策略,目前阶段,可用就行,后续真有资源问题再另行优化。

发布于: 刚刚阅读数: 2
用户头像

SkyFire

关注

这个cpper很懒,什么都没留下 2018.10.13 加入

会一点点cpp的苦逼码农

评论

发布
暂无评论
一个cpp协程库的前世今生(二十四)对象池与栈内存池