内存池解决的问题:1、提高申请和释放内存的效率 2、解决内存碎片
内存碎片:频繁申请、释放小块内存,可能会导致内存碎片。分为两种场景:内碎片,外碎片(通常)
高并发内存池:对比malloc在多线程并发场景下申请内存的性能,减少锁竞争——让每个线程都有一个自己独立的内存池。
内存池需要考虑以下的问题:
就是一个哈希映射的内存桶(自由链表),threadCache(线程缓存)是每个线程独有的,用于小于64k的内存的分配,线程从这里申请内存不需要加锁,每个线程独享一个cache,这也就是这个并发线程池高效的地方。
申请内存:
释放内存:
项目中用了静态TLS
为实现每个线程都拥有自己唯一的线程缓存,在threadCache中使用TLS(thread local storage)保存每个线程本地的threadCache的指针,这样threadCache在申请释放内存是不需要锁的,因为每一个线程都拥有了自己唯一的一个全局变量。
centralCache本质是由一个哈希映射的span对象自由双向链表构成,为了保证全局只有唯一的centralCache,这个类被可以设计成了单例饿汉模式,避免高并发下资源的竞争。
中心缓存是所有线程所共享,threadCache是按需从centralCache中获取的对象centralCache周期性的回收threadCache中的对象,避免一个线程占用了太多的内存,而其他线程的内存吃紧,达到内存分配在多个线程中更均衡的按需调度的目的。
申请内存:
释放内存:
页缓存是在centralCache缓存上面的一层缓存,存储的内存是以页为单位存
储及分配的,centralCache没有内存对象时,从pageCache分配出一定数量的page,并切
割成定长大小的小块内存,分配给centralCache。pageCache会回收centralCache满足条
件(usecount==0)的span对象,并且合并相邻的页,组成更大的页,缓解内存碎片的问题。
申请内存:
释放内存:
3. 如果centralCache向pageCache释放回一个span,则依次寻找该span的前后page id的span,看是否可以合并,如果可以合并继续向前寻找,直到不能合并为止。这样就可以将切小的内存合并收缩成大的span,减少内存碎片。
struct Span
{
PageID _pageId = 0; // 页号
size_t _n = 0; // 页的数量
Span* _next = nullptr; // 双向
Span* _prev = nullptr;
void* _list = nullptr; // 大块内存切小链接起来,这样回收回来的内存也方便链接
size_t _usecount = 0; // 使用计数,==0 说明所有对象都回来了
size_t _objsize = 0; // 切出来的单个对象的大小
};
Span意为跨度,管理着centralCache和pageCache里面的以页为单位的内存对象,可以实现对自由链表中元素的管理,当pageCache中给到threadCache里面的对象都回来之后(usecount为0时),就可以把Span归还到pageCache,且页号的设置可以对相邻空闲span进行合并,缓解了内存碎片的问题。
可以在pageCache中维护一个页号到Span的映射,当Span Cache给centralCache分配一个Span时,将这个映射更新到map中去,在Thread Cache还给Central Cache时,可以查这个std::map
找到对应的span。
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
void* ptr = VirtualAlloc(0, kpage*(1 << PAGE_SHIFT),
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
// brk mmap等
#endif
if (ptr == nullptr)
throw std::bad_alloc();
return ptr;
}
inline static void SystemFree(void* ptr)
{
#ifdef _WIN32
VirtualFree(ptr, 0, MEM_RELEASE);
#else
// sbrk unmmap等
#endif
}
当前实现的项目中我们并没有完全脱离malloc,比如SpanList中的span等结构,我们还是使用了new Span这样的操作,new的底层使用的是malloc,所以还不足以替换malloc,因为本身并没有完全脱离它。
解决方案:项目中增加一个定长的ObjectPool的对象池,对象池的内存直接使用brk、VirarulAlloc等向系统申请,new Span替换成对象池申请内存。这样就完全脱离malloc,替换掉malloc。
1.Linux等系统下面,需要将VirtualAlloc替换为brk等。
2.x64系统下面,当前的实现支持不足。比如:id查找Span得到的映射,我们当前使用的是
map
中心思想:对于在该内存池中,从系统申请的内存就会一直存在于这个内存池中,不会再归还给系统,从系统申请内存的时候是按页进行申请的,对于归还内存的时候,只需要判断该内存在哪一个页中,直接归还给包含这个页的span。我们做的只是将申请来的内存进行标记从而来使用内存,逻辑上对内存池进行分配。
项目源码:https://github.com/LumosN/ConcurrentMemoryPool