高性能内存池(一)----项目整体架构设计
1.概要
本项目是模拟学习tcmalloc(Thread-Caching Malloc,即线程缓存的malloc,出自google顶尖大佬之手)的巧妙思想和框架,主体为源项目的核心精华设计的简化版。
有兴趣的可以看看源码
文章主要内容:
本篇文章是该项目的第一章,主要介绍该项目的整体结构和每层小结构的部分细节,文章代码展示不多,但是其中的架构和思想至关重要
2.内存池整体结构
现代很多的开发环境都是多核多线程,在申请内存的场景下,必然存在激烈的锁竞争问题。malloc本身其实已经很优秀,那么我们项目的原型tcmalloc就是在多线程高并发的场景下更胜一筹,所以这次我们实现的内存池需要考虑以下几方面的问题:
- 性能问题。
- 多线程环境下,锁竞争问题。
- 内存碎片问题。
concurrent memory pool主要由以下3个部分构成:
-
thread cache:线程缓存是每个线程独有的,用于小于256KB的内存的分配。线程从这里申请内
存不需要加锁,每个线程独享一个cache,这也就是这个并发线程池高效的地方。 -
central cache:中心缓存是所有线程所共享,thread cache是按需从central cache中获取的对
象。central cache合适的时机回收thread cache中的对象,避免一个线程占用了太多的内存,而
其他线程的内存吃紧,达到内存分配在多个线程中更均衡的按需调度的目的。central cache是存
在竞争的,所以从这里取内存对象是需要加锁,首先这里用的是桶锁,其次只有thread cache的
没有内存对象时才会找central cache,所以这里竞争不会很激烈。 -
page cache:页缓存是在central cache缓存上面的一层缓存,存储的内存是以页为单位存储及分
配的,central cache没有内存对象时,从page cache分配出一定数量的page,并切割成定长大小
的小块内存,分配给central cache。当一个span的几个跨度页的对象都回收以后,page cache
会回收central cache满足条件的span对象,并且合并相邻的页,组成更大的页,缓解内存碎片
的问题。
看到这你可以会有很多question,但是不用担心,先继续看,看到后面就明白了
结构图如下:
3.线程缓存结构详解
线程缓存实际上是一个哈希桶,每个桶是一个按桶位置映射大小的内存块对象的自由链表
申请内存步骤:
1.当申请内存size<=256KB,先获取线程本地存储的ThreadCache对象,然后通过size映射到对应的哈希桶自由链表。
2.如果自由链表中有小块儿内存,则直接返回给线程使用
3.如果自由链表为空,向中心缓存申请内存并返回给线程
释放内存:
线程将某个大小的内存块返回给大小所映射到的哈希桶里的自由链表,满足一定条件时ThreadCache就会将小块儿内存还给CentralCache。
4.中心缓存结构
中心缓存(CentralCache)本质上也是一个哈希桶,它的映射方式和ThreadCache类似,不同的是,哈希桶中存储的是span组成的带头双向循环链表,其中每个span又分成小块内存组成的单链表。
span是一个由连续内存页组成的结构体,负责管理大块内存。实际上,中心缓存的span实际上是由页缓存分配的,当一个span满足某种条件时,它会被还给页缓存,
// 管理多个连续页大块内存跨度结构
struct Span
{PAGE_ID _pageId = 0; // 大块内存起始页的页号size_t _n = 0; // 页的数量Span* _next = nullptr; // 双向链表的结构Span* _prev = nullptr;size_t _objSize = 0; // 切好的小对象的大小size_t _useCount = 0; // 切好小块内存,被分配给thread cache的计数void* _freeList = nullptr; // 切好的小块内存的自由链表bool _isUse = false; // 是否在被使用
};
5.页缓存结构
页缓存的结构也是一个哈希桶,与前两个不同的是,页缓存的映射方式是通过页的数量来映射到span链表。如下图:
页缓存的span和中心缓存的span共用同一个span结构,不同的是,页缓存的span是直接从系统申请的大块空间,这些span会直接分配给中心缓存去切分和分配。
由于需要遍历,页缓存是需要加互斥锁的
当ThreadCache没有小块内存并且CentralCache中没有span时,CentralCache会向PageCache申请内存,假设此时申请的是8page的span,则PageCache会从8号桶中查看是否有span,若有就返回给CentralCache,如果没有就遍历9-128号桶,如果在某个桶找到空闲的span(假设是110号),就会把这个110page的span切分成8page的span(随后返回给CentralCache)和102page的span,并分别挂到对应的桶上,如果遍历后没有,就会向操作系统申请一个128page的空间,并按照上述过程切分成两个span。
6.总结
这个三层结构设计的巧妙之处在于,ThreadCache不需要加锁,CentralCache只需要加桶锁,也就是不同的线程进入到同一个桶时才会有锁互斥,这在如今常见的多核高并发场景下是有非常好的性能的!