文章目录
内存池
内存池是一种动态分配与管理技术。当我们使用new/delete 或者malloc/free的时候,系统都会在堆上分配相应的内存给我们。当我们频繁的去申请内存释放内存时,就可能会造成内存碎片的问题。所以内存池会把我们可能会用到的资源先申请出来,放在一个池内管理,这样可以提高资源的使用效率,也可以让每个线程都有相应的资源数量。当我们申请资源时,直接在内存池中获取,释放资源时将资源放进池中。
内存碎片问题
如果我们频繁的去申请内存、释放内存时,就会让一段原本连续的内存被切成很多细小的碎片。例如我们向图中这样申请释放内存。
当我们释放完内存后,我们一共有8+8+4=20kb内存可以使用,但我们却不能在申请出一块连续的16kb的内存。这就是内存碎片的问题。尽管看起来我们有很多内存。却不能很好的使用。这个问题叫做“外碎片”。
那么相对的就会有内碎片问题。 我们再向系统申请内存时,假如我们需要三字节。但是系统为了方便管理。他并不会真的给我们三字节的内存。他可能会给我们四字节或者八字节。那我们却只是用三字节内存。那么剩余的部分也会被浪费掉。这种内存碎片我们称为“内碎片”。
申请效率的问题
因为我们申请内存是需要去调用系统IO的,而每次我们都需要从用户态切换到内核态,还需要程序上下文的切换,这些都是很耗费资源的。所以如果我们使用内存池,就可以先申请一批内存,当我们需要使用的时候,在自己去分配已经拿到的那部分内存。就会提高我们的申请效率。
malloc概述
其实malloc也有一个内存池结构。malloc在底层使用了分离适配。 分配器维护了一个空闲链表数组。每个空闲链表包含大小不同的内存块。当要分配一个内存时,我们对相应的空闲聊表查找合适的内存块。如果没有相应大小的内存块了,则在更大的内存块对应链表进行搜索,找到合适的进行切割处理。 如果所有的空闲链表都没有找到。则向操作系统申请一块大内存,然后在新的内存中分配一块。
在释放内存时,则将内存重新放回相应的空闲链表中。
并发内存池的设计概述
因为我们很多开发都时需要用多线程,高并发的情况。那这种时候我们就需要更多的去考虑线程安全,资源竞争的问题。所以我们除了再考虑到内存碎片的处理问题和性能外,还需要考虑再多核多线程的环境下,考虑到锁竞争时,如何让效率更快一点。
在考虑到这些情况下,我们的内存池主要有三个部分构成:
- thread cache :线程缓存是每个线程独立占有的,当我们申请一些小内存时,线程就可以从这里申请内存使用,这样就不需要加锁。因为每个线程都可以独享一个cache ,这样就会让申请效率提高,同时不用担心锁竞争问题。
- central cache:中心缓存是所有线程共享的资源,thread cache从这里获取内存块。central cache负责回收从thread cache中被释放的大量内存,然后可以将这些内存分配给其他的线程,这样可以避免一个线程占用太多的内存资源。
- page cache 页缓存是central cache的上层缓存,负责管理以页为单位的大块内存,当central cache没有内存资源时,从page cache中划分资源交给central cache 。在页缓存中,我们还需要将切碎但是释放的内存合并,组成更大的内存,这样就可以有效的缓解内存碎片的问题。
common类
在整个结构中,我们需要一些共通的数据结构来帮助我们管理和分配内存。例如确定对应size大小内存块的使用情况的span,管理内存分配的自由链表freelist,以及我们还需要记录每个块内存对应的span,管理他们的映射关系。
自由链表freelist
自由链表是我们用来管理不同大小内存块和span的主要数据结构,我们需要用自由链表来管理各个线程的私有缓存。通过自由链表和哈希映射,我们也能较高效率的拿到我们想要大小的内存块。
class FreeList
{
public:
void Push(void* obj) {
NextObj(obj) = _head;
_head = obj;
++_size;
}
void* Pop() {
void* obj = _head;
_head = NextObj(_head);
--_size;
return obj;
}
/// <summary>
/// 插入n个
/// </summary>
/// <param name="start"></param>
/// <param name="end"></param>
/// <param name="n"></param>
void PushRange(void* start, void* end,size_t n) {
NextObj(end) = _head;
_head = start;
_size += n;
}
/// <summary>
/// 将n个拿出来
/// </summary>
/// <param name="start"></param>
/// <param name="end"></param>
/// <param name="n"></param>
void PopRange(void*& start,void*& end ,int n) {
start = _head;
for (int i = 0; i < n;++i) {
end = _head;
_head = NextObj(_head);
}
NextObj(end) = nullptr;
_size -= n;
}
bool Empty() {
return _head == nullptr;
}
size_t Size() {
return _size;
}
size_t MaxSize() {
return _max_size;
}
void SetMaxSize(size_t size){
_max_size = size;
}
private:
void* _head = nullptr;
size_t _size = 0;
//要对象的频率
size_t _max_size = 1;
};
内存管理span
span则是我们管理批量内存块的数据结构,
threadcache
thread cache为每个线程私有,在线程缓存中我们维护一个自由链表,来存放当前线程可用的内存块。当我们申请小内存时。找到对应size大小的自由链表,从自由链表中拿取一个内存块使用,这样也没有锁竞争。 当自由链表中没有对象时,则批量从central cache中获取一定数量的对象,插入到自由链表中,然后取一个对象线程用。
class ThreadCache
{
public:
/// <summary>
/// 申请内存大小
/// </summary>
/// <param name="bytesize"></param> 申请size大小内存
/// <returns></returns> 返回指向地址的指针
void* Allocate(size_t bytesize);
/// <summary>
/// 释放内存
/// </summary>
/// <param name="bytesize"></param> 释放大小
void Deallocate(void* ptr,size_t bytesize);
/// <summary>
/// 从中心缓存获取对象
/// </summary>
/// <param name="index"></param> 获取对象放在哪个链表
/// <param name="bytesize"></param> 获取对象大小
void* FetchFromCentralCache(size_t index, size_t bytesize);
/// <summary>
/// 释放对象时,回收长链表至中心缓存
/// </summary>
/// <param name="list"></param>
/// <param name="size"></param>
void ListTooLong(FreeList& list, size_t bytesize);
private:
FreeList _freelist[NFREELIST];
};
TLS
TLS (thread local storage)可以保存每个线程本地的threadcache指针,用tls的变量会使每个线程单独保存一份自己的指针,当线程需要访问这个变量时,会通过这个指针去访问,而每个线程的指针只向自己的这个数据。所以即使不上锁,各个线程也不会引起线程安全问题。
同时,TLS分为静态和动态的。我们这里可以通过定义一个静态的TLS就可以实现我们的需求。
static _declspec(thread) ThreadCache* tls_threadcache = nullptr;
centralcache
当thread cache中没有内存时,就会批量向central cache申请一些内存对象,central cache 也有一个哈希映射的自由链表, 只不过这里的自由链表不是挂着的不是内存块,而是一个完整span,一个span会完整的管理数页大小的内存。 从对应size的链表中找一个可用的span,获取一部分内存块分给thread cache。
当central cache中没有找到合适的非空的span时,则再向上找page cache申请一个span对象。切成对应size的大小块,并链接起来,挂到span中。最后交给thread cache。
在释放内存时,当thread cache过长或者释放线程后,我们需要将thread cache中占用的内存资源回收进central cache中,释放回来时,我们统计各个span内的使用个数相应减少。当一个span的使用个数为零时,则表示这个span内所有对象都全部释放了。这样就可以将这个span合并会page cache ,以便于更大内存的合并和申请。
typedef size_t pageID;
class CentralCache
{
public:
/// <summary>
/// 从中心缓存中获取一定范围对象对象
/// </summary>
/// <param name="start"></param> 获取内存起始位置
/// <param name="end"></param> 获取内存结束位置
/// <param name="n"></param> 对象个数
/// <param name="bytesize"></param> 单个对象大小
/// <returns></returns>
size_t FetchRangeObj(void*& start, void*& end, size_t n, size_t bytesize);
/// <summary>
/// 向pagecache获取内存
/// </summary>
/// <param name="list"></param> 获取内存挂在list上
/// <param name="bytesize"></param> 获取内存的大小
/// <returns></returns>
Span* GetOneSpan(SpanList& list, size_t bytesize);
/// <summary>
/// 将一定数量的对象放回对象大小span
/// </summary>
/// <param name="start"></param>
/// <param name="byte_size"></param>
void ReleaseListToSpan(void* start, size_t byte_size);
static CentralCache* GetCentralCache() {
return ¢ralcache;
}
private:
CentralCache() = default;
CentralCache(const CentralCache&) = delete;
CentralCache(const CentralCache*) = delete;
SpanList _spanlist[NFREELIST];
static CentralCache centralcache;
};
在central cache中,我们使用了单例模式,因为一个多个线程需要公用一个central cache ,这样central cache才能起到调度各个线程占用资源的比例。所以在上面的声明中,我们将central cache的几个构造函数私有并加上了delete关键词。
pagecache
pagecache是一个以页单位的span自由链表,为了保证全局唯一性,在pagecache中也使用了单例模式。
当central cache向page cache申请内存时,page cache先检查对应位置有没有span,如果没有则向更大页寻找一个span,如果找到则分裂成两个,一个为我们需要大小的对应的span,剩下一个span则挂在对应位置。 如果最大位置(128page)都没有找到span,则向系统申请一个大片内存(128页)每次申请都申请最大值(128),从而尽量减少去向系统申请内存的次数。 申请到一个大片内存后,再重新寻找合适的span。
如果central cache释放回一个span,则一次寻找span的前后page id的span,如果前面或者后面对应的span也没有被使用,则可以合并出一个更大的span,如果合并成功则继续向前向后合并,这样就可以减少内存碎片。
class PageCache
{
public:
/// <summary>
/// 获取一片napge页内存
/// </summary>
/// <param name="npage"></param>
/// <returns></returns>
Span* NewSpan(size_t npage);
//Span* _NewSpan(size_t npage);
/// <summary>
/// 向系统要内存
/// </summary>
/// <param name="npage"></param>
void* SystemAllocPage(size_t npage);
/// <summary>
/// 获取从对象到span的映射关系
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
Span* MapObjectToSpan(void* obj);
/// <summary>
/// 返回id对应span的bytesize
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
size_t GetIdToSize(pageID id);
/// <summary>
/// 设置id对应span的切割bytesize
/// </summary>
/// <param name="id"></param>
/// <param name="bytesize"></param>
/// <returns></returns>
void SetIdToSize(pageID id, size_t bytesize);
/// <summary>
/// 向pagecache合并 前后检查是否可以合并出更大的空间
/// </summary>
/// <param name="span"></param>
void ReleaseSpanToPageCache(Span* span);
static PageCache* GetPageCache() {
return &pagecache;
}
private:
PageCache() = default;
PageCache(const PageCache&) = delete;
PageCache(const PageCache*) = delete;
/// <summary>
/// 按页数映射的spanlist
/// </summary>
SpanList _spanlist[NPAGES];
// 基数树
TCMalloc_PageMap1<32 - PAGE_SHIFT> _idSizeMap;
TCMalloc_PageMap2<32 - PAGE_SHIFT> _idSpanMap;
//std::unordered_map<pageID, Span*> _idSpanMap;
static PageCache pagecache;
public:
std::recursive_mutex _mtx;
};
基数树
在我们上面的pagecache中,为了保存内存块和span之间的映射关系,我们使用了TCMallocPageMap,他的底层是基数树,在一般情况下,我们也可以利用STL库中的unordered_map来保存这些映射关系,但是在pagecache中,我们每次去改变这些映射关系时,有可能会有其他线程来访问这些映射关系,如果我们使用unordered_map时,为了线程安全,我们需要对pagecache加锁。以免出现冲突。
但是通过基数树,我们将所有的内存块分开放在一个Sizemap和Spanmap中来保存内存块与span之间的映射关系。 因为在基数树中,每块内存都有一个位置来存放相应的映射关系
- 当我们需要修改这些映射关系时,有可能是在一片大内存被分配切割时,这时我们需要将这部分内存提交给centralcache,所以这时候是不会有别的线程来访问这个内存块对应的映射关系的,因为我们还没有将这些内存块交给对应的线程使用。
- 当我们需要访问这些映射关系时,是当这些内存从centralcache返还给pagecache时,这时我们可能需要将前后页的内存合并,那么此时必然不会有其他的线程需要去分配这片内存的映射关系,因为这块内存还不能被使用。
所以我们利用基数树来保存内存块的映射关系时,可以不需要对pagecache加这把大锁。
而如果加锁的话会导致上下文的切换等等额外开销,所以使用基数树可以很大的提升我们在高并发情况下的效率。
template<int BITS>
class TCMalloc_PageMap1 {
private:
static const int LENGTH = 1 << BITS;
size_t* _array;
public:
typedef uintptr_t Number;
TCMalloc_PageMap1() {
_array = reinterpret_cast<size_t*>(malloc(sizeof(size_t) << BITS));
memset(_array, 0, sizeof(size_t) << BITS);
}
bool ensure(Number x, size_t n) {
return n <= LENGTH - x;
}
void PreallocateMoreMemory(){}
size_t get(Number k)const {
if ((k >> BITS) > 0)
return 0;
return _array[k];
}
void set(Number k, size_t v) {
_array[k] = v;
}
size_t Next(Number k)const {
while (k < (1 << BITS)) {
if (_array[k] != nullptr)
return _array[k];
k ++ ;
}
return nullptr;
}
};
template<int BITS>
class TCMalloc_PageMap2
{
private:
static const int ROOT_BITS = 5;
static const int ROOT_LENGTH = 1 << ROOT_BITS; //32
static const int LEAF_BITS = BITS - ROOT_BITS;
static const int LEAF_LENGTH = 1 << LEAF_BITS; // 2^15
//leaf node
struct Leaf {
Span* values[LEAF_LENGTH];
};
Leaf* _root[ROOT_LENGTH];
public:
typedef size_t Number;
explicit TCMalloc_PageMap2() {
memset(_root, 0, sizeof(_root));
PreallocateMoreMemory();
}
/// <summary>
/// 预分配内存
/// </summary>
void PreallocateMoreMemory() {
Ensure(0, 1 << BITS);
}
/// <summary>
/// 开基数树所需全部内存
/// </summary>
/// <param name="start"></param>
/// <param name="n"></param>
/// <returns></returns>
bool Ensure(Number start, size_t n) {
for (Number key = start; key <= start + n - 1;) {
const Number i1 = key >> LEAF_BITS;
if (i1 >= ROOT_LENGTH)
return false;
if (_root[i1] == nullptr) {
Leaf* leaf = new Leaf;
if (leaf == nullptr)
return false;
memset(leaf, 0, sizeof(leaf));
_root[i1] = leaf;
}
key = ((key >> LEAF_BITS) + 1) << LEAF_BITS;
}
return true;
}
Span* get(Number k)const {
const Number i1 = k >> LEAF_BITS;
const Number i2 = k & (LEAF_LENGTH - 1);
if ((k >> BITS) > 0 || _root[i1] == nullptr) {
return nullptr;
}
return _root[i1]->values[i2];
}
void set(Number k, Span* v) {
const Number i1 = k >> LEAF_BITS;
const Number i2 = k & (LEAF_LENGTH - 1);
assert(i1 < ROOT_LENGTH);
_root[i1]->values[i2] = v;
}
Span*& operator[](Number k) {
const Number i1 = k >> LEAF_BITS;
const Number i2 = k & (LEAF_LENGTH - 1);
assert(i1 < ROOT_LENGTH);
return _root[i1]->values[i2];
}
void erase(Number k) {
const Number i1 = k >> LEAF_BITS;
const Number i2 = k & (LEAF_LENGTH - 1);
assert(i1 < ROOT_LENGTH);
_root[i1]->values[i2] = nullptr;
}
void* Next(Number k) const {
while (k < (1 << BITS)) {
const Number i1 = k >> LEAF_BITS;
Leaf* leaf = _root[i1];
if (leaf != nullptr) {
for (Number i2 = k & (LEAF_LENGTH - 1); i2 < LEAF_LENGTH; i2++) {
if (leaf->values[i2] != nullptr) {
return leaf->values[i2];
}
}
}
k = (i1 + 1) << LEAF_BITS;
}
return nullptr;
}
};