高并发内存池

一、项目背景介绍

本项目的原型是Google开源的tcmalloc项目,tcmalloc全称为Thread Cache Malloc,意为线程缓存的malloc,用于替换系统的malloc和free。其替换原因在于malloc在对于多线程申请内存时的效率较低,使用该方法能够提高大概20%的效率。

二、项目整体架构

整个项目是一个三级缓存的结构,第一层为ThreadCache,第二层为CentralCache,第三层为PageCache,其代码实现也是从第一层,逐步走到第三层,请大家耐心观看。

三、理解内存池是什么

内存池是指程序预先操作系统申请一块足够大的内存,此后,当程序中需要申请内存的时候,不是直接向操作系统申请,而是直接从内存池中获取;同理,当释放内存的时候,并不是真正将内存返回给操作系统,而是将内存返回给内存池。当程序退出时(或某个特定时间),内存池才将之前申请的内存真正释放。

四、开胃菜——定长内存池

定长内存池顾名思义,就是一个长度固定的内存块,其只能申请和释放固定大小的内存块。因此我们可以将它的性能发挥到机制。设计定长内存池的目的也是为后面打好基础。

4.1内存申请的管理

首先设计不考虑内存释放时的场景!

上述申请的过程,可能有以下疑虑:

  1. 为什么每次申请的大小都是T呢? 这就是定长内存池的特点,每次申请都是固定大小的,这样方便管理!

  1. 只能一块一块的申请吗?是的,目前只能一块一块的申请,后续可以扩展

  1. 被切分出去的内存块是连续的吗?答案是:一定是连续的!

上述过程中,并没有考虑内存释放的问题!下面先让我们看看内存释放!最后再做总结。

template<class T>
class ObjectPool
{
public:
    //构造函数很容易理解,刚开始没有内存块,并且剩余字节数为0
    ObjectPool():_memory(nullptr),_freelist(nullptr),_remainBytes(0){}
    T* New()
    {
        T* obj = nullptr;//obj用于申请的内存块的起始地址
        //每次只能申请一块大小为T的内存块
        //如果T > 剩余内存的数量,就需要重新申请一块定长的内存块(很容易理解叭)
        if (sizeof(T) > _restnum)
        {
            _memory = (char*)malloc(128 * 1024);
            _remainBytes = 128 * 1024;
        }
        //计算objSize与一个指针的大小,这么做是为了后面释放内存的时候方便!可以暂时记住。
        int objSize = sizeof(T) > sizeof(void*) ? sizeof(T) : sizeof(void*);
        //_memory是剩余内存块的头,将_memory给obj,_memory向后移objSize个单位
        //因为_memory是char*类型,所以申请多少内存,就移多少个单位。
        //如果还不理解的话,就看一下画的图。
        obj = (T*)_memory;
        _memory += objSize;
        //此时剩余字节数减掉刚刚申请的内存块的数量。
        _remainBytes -= objSize;
        new(obj)T;//定位new,显示调用T的构造函数的初始化
        return obj;
    }
private:
    char* _memory; //大块内存的起始地址
    void* _freelist;//暂时不考虑它!
    size_t _remainBytes;  //剩余字节数
};

4.2内存释放的管理

被释放掉的内存,我选择通过自由链表来进行管理。那么什么是自由链表呢?

自由链表:看起来就是一个链表,但其并没有显性设置链表的结点next,而是将next存在了当前内存块的前4位或8位地址。这样说比较抽象,不如来看看图解。

后面被释放的内存依旧按照这个流程!

void Delete(T* obj)
    {
         obj->~T();//显示调用obj的析构函数
         *(void**)obj = _freelist; 
        //强转为void**,再解引用,将_freelist的值放入obj的头4个或8个字节。
        //为什么是4或者8字节呢?因为对应32位和64位系统,指针大小可能不同。
        //再更新_freelist
         _freelist = obj;
    }

4.3 完善内存申请的管理

现在考虑到有释放的内存,那么将释放的内存进行回收,是可以进行再利用的!

因此现在内存申请的流程,应该是先看_freelist里面是否有回收的内存,如果有的话,先申请_freelist里面的内存。否则再到内存池中进行申请!

完整代码:

template<class T>
class ObjectPool
{
public:
    ObjectPool() :_memory(nullptr), _freelist(nullptr), _remainBytes(0) {}
    T* New()
    {
        T* obj = nullptr;
        if (_freelist)
        {
            void* next = *(void**)_freelist;//这里是为了取到当前内存块保存的下一块内存的地址。
            obj = _freelist;
            _freelist = next;
        }
        else 
        {
            if (sizeof(T) > _remainBytes)
            {
                _memory = (char*)malloc(128 * 1024);
                _remainBytes = 128 * 1024;
            }    
        }
        //计算objSize与一个指针的大小,这么做是为了后面释放内存的时候方便!可以暂时记住。
        int objSize = sizeof(T) > sizeof(void*) ? sizeof(T) : sizeof(void*);
        obj = (T*)_memory;
        _memory += objSize;
        _remainBytes -= objSize;
        new(obj)T;
        return obj
    }

    void Delete(T* obj)
    {
        obj->~T();
        *(void**)obj = _freelist;
        _freelist = obj;
    }
private:
    char* _memory;
    void* _freelist;
    size_t _remainBytes;
};

五、ThreadCache的设计

5.1 ThreadCache整体框架

ThreadCache的结构是一个哈希桶结构,每一个哈希桶对应不同的内存大小。需要申请多少内存,就到对应的桶里面去取一块内存就可以了。并且每个线程都有自己独立的ThreadCache,不需要加锁处理,这也是能够提高效率的原因之一。

申请内存:

1. 当内存申请size<=256KB,先获取到线程本地存储的thread cache对象,计算size映射的哈希桶自

由链表下标i。

2. 如果自由链表_freeLists[i]中有对象,则直接Pop一个内存对象返回。

3. 如果_freeLists[i]中没有对象时,则批量从central cache中获取一定数量的对象,插入到自由链表

并返回一个对象。

疑问环节

疑问1:为什么不能给每个大小比如(1-7)都设置一个桶?

答案:如果每个大小都设置一个桶的话,桶的数量将非常的庞大,不利于管理

疑问2:如果这个桶里面没有内存了怎么办?

答案:如果对应的桶没有了内存,就需要到下一层CentralCache中去申请内存。

疑问3:为什么每个桶的大小一定是8的整数?不能是4的整数或者5、6、7的整数倍呢?

答案:根据定长内存池的设定,其每一个内存块至少要有一个指针大小。在32位和64位下,不同的数据类型的指针大小可能不同,因此最小设立为8。

疑问4:如果一次性申请的内存大于256KB怎么办?

答案:后面会讲解,这里暂不考虑。

5.2代码框架:现只考虑申请内存的流程

static const size_t NFreelist = 208; //一共有208个桶。
static const size_t MAX_BYTES = 256 * 1024;

//在设计定长内存池中讲过,如何将两个内存块连接起来。这里是封装了一部分代码!
static void*& NextObj(void* obj) 
{
    assert(obj);
    return *(void**)obj;
}

//Freelist 是用来管理哈希桶的,后面在CentralCache中也会用到。大家可以先理解一下
class FreeList
{
public:
    void Push(void* obj)
    {    
        NextObj(obj) = _freelist;
        _freelist = obj;
        _size++;
    }
    void PushRange(void* start, void* end, size_t n)
    {
        NextObj(end) = _freelist;
        _freelist = start;
        _size += n;
    }
    void* pop()
    {
        void* ret = _freelist;
        _freelist = NextObj(_freelist);
        _size--
        return ret;
    }
    bool Empty()
    {
        return _freelist == nullptr;
    }
public:
    void* _freelist = nullptr;
    size_t Maxsize = 1;
    size_t _size = 0;
};

class ThreadCache
{
public:
    void* Allocate(size_t size);//申请内存
    void* FetchFromCentralCache(size_t index, size_t size);//向Central Cache申请内存
private:
    FreeList _freelist[NFreelist];
};

代码均有注解,如果对接口不明白的,我会在接下来的代码写上注释。

void* Allocate(size_t size) 申请内存的接口
{
    size_t alignSize = SizeClass::RoundUp(size);
    size_t index = SizeClass::Index(size);
    if (!_freelist[index].Empty())//这里是判断当前桶是否为空,如果不为空,则直接到桶中取内存。
    {
        return _freelist[index].pop();
    }
    return FetchFromCentralCache(index, alignSize);//桶为空,则要到第二层,中间缓存层去取内存。
}

图解:在ThreadCache中申请内存

是滴,从ThreadCache中申请内存的流程就结束了,至于最后需要从第二层中去取的代码,我将会放到介绍完了第二层CentralCache以后,再进行完善!

卖个关子,前面不是提到了,每个线程都具有独立的ThreadCache吗?是的,但是现在还没有写上去!等后面继续完善。

六、CentralCache的设计

6.1 CentralCache的整体框架

CentralCache依旧是哈希桶的结构,每个桶里面有若干的Span,每一个Span都具有一个freelist用于管理内存块。

申请内存:

1. 当thread cache中没有内存时,就会批量向central cache申请一些内存对象,这里的批量获取对

象的数量使用了类似网络tcp协议拥塞控制的慢开始算法;central cache也有一个哈希映射的

spanlist,spanlist中挂着span,从span中取出对象给thread cache,这个过程是需要加锁的,不

过这里使用的是一个桶锁,尽可能提高效率。

2. central cache映射的spanlist中所有span的都没有内存以后,则需要向page cache申请一个新的

span对象,拿到span以后将span管理的内存按大小切好作为自由链表链接到一起。然后从span

中取对象给thread cache。

3. central cache的中挂的span中use_count记录分配了多少个对象出去,分配一个对象给thread

cache,就++use_count

6.2 CentralCache的代码结构,现只考虑申请内存的流程

CentralCache采取的是单例模式的设计,所有的线程都是共用同一个CentralCache!
class CentralCache
{
public:
    static CentralCache* GetInstance()
    {
        return &_sInit;
    }
    size_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size);
    
private:
    CentralCache() {};
    CentralCache(const CentralCache& ) = delete;
    static CentralCache _sInit;
    Spanlist _spanlist[NFreelist];
};
CentralCache CentralCache::_sInit;

struct Span
{
    Span* _next = nullptr;      双向循环链表的结构
    Span* _prev = nullptr;
    size_t _objsize = 0;   切好内存的大小,单位是字节
    size_t _useCount = 0;  切好小块内存,被分配给thread cache的计数
    void* _freelist = nullptr; 每一个span都具有一个自由链表,来管理内存块。

    下面的暂时不用管!
    PAGE_ID _pageId = 0; 大块内存起始页的页号
    size_t _n = 0; 页的数量
    bool _IsUse = false;
};

class Spanlist
{
public:
    Spanlist()
    {
        _head = new Span;
        _head->_next = nullptr;
        _head->_prev = nullptr;
    }

private:
    Span* _head = nullptr;
public:
    std::mutex _mtx; 
    每一个Spanlist 都具有一个锁,因为CentralCache是只有一个的。会存在多个线程共同竞争同一个桶的情况,因此需要加锁
};

接口的实现:

补充ThreadCache中的:FetchFromCentralCache 从CentralCache中拿内存
void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{
    size是申请的内存大小,index是桶的编号
    size_t batchNum = min(_freelist[index].MaxSize(), SizeClass::NumMoveSize(size));
    if (batchNum == _freelist[index].MaxSize())
    {
        _freelist[index].Maxsize++;
    }
    void* start = nullptr;
    void* end = nullptr;
    实际申请到的内存块数量
    
    int actualNum = CentralCache::GetInstance()->FetchRangeObj(start, end, batchNum, size);
    再将申请到的内存块插入到对应的桶中
    为什么要判断这一步呢?
    因为如果 actualNum等于1的话,就不需要再插入到桶中了!
    if (actualNum == 1)
    {
        assert(start == end);
    }
    else
    {
        //插入桶中的时候,一定是先将start给排除了,再插入的。
        _freelist[index].PushRange(NextObj(start), end, actualNum - 1);
    }
    return start;
}

在CentralCache中截取部分内存
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size)
{
    int index = SizeClass::Index(size); 找到对应的桶号
    _spanlist[index]._mtx.lock(); 因为要对桶进行操作,所以需要加锁
    
    要找到一个不为空的span
    Span* span = CentralCache::GetInstance()->GetOneSpan(_spanlist[index],size);
    assert(span);
    assert(span->_freelist);
    
    开始截取
    start = span->_freelist;
    void* cur = start;
    size_t actualNum = 0;
    while (cur && batchNum != 0)
    {
        end = cur;
        cur = NextObj(cur);
        actualNum++;
        batchNum--;
    }

    span->_freelist = cur;
    span->_useCount += actualNum; span中已经使用的内存块

    _spanlist[index]._mtx.unlock();对桶的操作已经结束了,解锁!
    return actualNum;
}

获取一个非空的Span
Span* CentralCache::GetOneSpan(Spanlist& spanlist, size_t size)
{
    size_t index = SizeClass::Index(size);
    Span* start = spanlist.Begin();
    while (start != spanlist._head)
    {
        if (start->_freelist != nullptr)
            return start;
        start = start->_next;
    }
此时这里需要解锁,因为现在并不对spanlist进行操作了,虽然现在spanlist是空的,但是可能会有内存的释放,回收问题。如果不解锁,会影响内存回收。
    spanlist._mtx.unlock();

    走到这里说明Spanlist是空的
    需要向PageCache申请内存。
    int k = SizeClass::NumMovePage(size);
    向PageCache申请内存是需要加锁的。
    PageCache::GetInstance()->_mtx.lock();
    Span* span = PageCache::GetInstance()->NewSpan(k);
    PageCache::GetInstance()->_mtx.unlock();
    标记这个span正在使用
    span->_IsUse = true;
    span->_objsize = size;
    此时是切分了一个很大的span 不是CentralCache中所需要的span
    我们是需要将这个span 进行切割的,放入到自由链表中。
    通过页号计算出 span的起始位置
    char* start = (char*)(span->_pageId << PAGE_SHIFT);
    再计算这个span一共有多大。
    size_t bytes = span->_n << PAGE_SHIFT;
    再计算出结尾的位置!
    char* end = start + bytes;
    将start作为自由链表的头
    span->_freelist = start;
    start += size;

    再将剩余部分进行尾插
    void* tail = span->_freelist;
    while (start != end)
    {
        NextObj(tail) = start;
        tail = NextObj(tail);
        start += size;
    }
    NextObj(tail) = nullptr;
    spanlist._mtx.lock();
    spanlist.PushFront(span);
    return span;
}

图解:如何在CentralCache中申请内存:

(1)找到对应的桶并找到一个非空的Span

(2)如果在PageCache中申请一个新的span

新申请的newspan是需要处理后,再放入Central Cache的

(3)对该非空Span进行分割:

①:batchNum小于等于Span中内存块的数量

②:batchNum大于Span中内存块的数量

(4)将申请多的内存块挂到ThreadCache中

如何理解申请多的内存块?因为每一次向CentralCache申请内存的时候,不是一块一块的申请,而是一批一批的申请内存块,这样可以减少对CentralCache的访问。因为每次对CentralCache进行访问的时候,都需要加锁和解锁的操作。因此需要一次性申请多块内存。

七、PageCache的设计

7.1 PageCache的整体框架

PageCache是一个以页为单位的spanlist。

申请内存:

1.当central cache向page cache申请内存时,page cache先检查对应位置有没有span,如果没有则向更大页寻找一个span,如果找到则分裂成两个。比如:申请的是4页page,4页page后面没有挂span,则向后面寻找更大的span,假设在10页page位置找到一个span,则将10页pagespan分裂为一个4页page span和一个6页page span。

2.如果找到_spanList[128]都没有合适的span,则向系统使用mmap、brk或者是VirtualAlloc等方式申请128页page span挂在自由链表中,再重复1中的过程。

7.2 PageCache代码框架 现只考虑申请内存的流程

将PageCache设计为单例模式
class PageCache
{
public:
    static PageCache* GetInstance()
    {
        return &_sInit;
    }
    Span* NewSpan(size_t k);
private:
    PageCache(){}
    PageCache(const PageCache&) = delete;
private:
    Spanlist _pagelist[NPAGES];用于管理每一页的自由链表。NPAGES =129;因为没有第0页,但数组下标是从0开始的。因此一共设置了129页,不用第0页。
public:
    static PageCache _sInit;
    std::mutex _mtx; PageCache只有一把整体的锁,是因为回收内存的时候,每个页之间会相互影响,因此得整体加锁。为什么不每个page都加锁呢?这样的话会降低整体的效率,访问每一页的时候都需要加锁解锁,性能不好。
};
PageCache PageCache::_sInit;

接口实现:

class Spanlist
{
    void Erase(Span* pos)
    {
        assert(pos);
        assert(pos!=_head);
        Span* next = pos->_next;
        Span* prev = pos->_prev;
        next->_prev = prev;
        prev->_next = next;
    }
    Span* PopFront() 头删,这里的删只是把当前Span从Spanlist中删除,并不是将它delete!
    {
        Span* front = _head->_next;
        Erase(front);
        return front;
    }    
    void PushFront(Span* span)  头插
    {
        Span* next = _head->_next;
        _head->_next = span;
        span->_prev = _head;
        next->_prev = span;
    }
}

Span* PageCache::NewSpan(size_t k)
{
如果一次性要的内存是大于128页的,那么直接向系统申请内存。
    if (k > NPAGES - 1)
    {
        Span* span = new Span;
        void* ptr = SystemAlloc(k);
        span->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
        span->_n = k;
        return span;
    }
    if (_pagelist[k].Empty())如果这一页为空,则往后面找不是空白页的地方
    {
        for (int i = k + 1; i < NPAGES; i++)
        {
            if (!_pagelist[i].Empty())
            {
                Span* nspan = _pagelist->PopFront();将这一部分内容弹出来
                Span* kspan = new Span;
设置页号,可能有的同学会好奇,这里的页号在哪呢?仔细慢慢看
                kspan->_pageId = nspan->_pageId;
页数为k
                kspan->_n = k;
                nspan->_pageId += k;
                nspan->_n -= k;
                _pagelist[i - k].PushFront(nspan);
                return kspan;
            }
        }
        到这里说明_pagelist[128]都为空,这时需要向系统申请空间
        Span* bigSpan = new Span;
        void* ptr = SystemAlloc(NPAGES - 1);自己封装的向系统申请空间
        bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;计算页号的公式
        bigSpan->_n = NPAGES - 1;页号填充。
        _pagelist[bigSpan->_n].PushFront(bigSpan); 
        申请完毕后,再递归一次,此时的_pagelist[128]不为空白页,可以进行切割了。
        return NewSpan(k);
    }
    代码走到这里说明当前页不为空页,可以直接弹回Span*
    else
    {
        return _pagelist[k].PopFront();
    }
}

图解:向PageCache中申请内存

以上就是申请内存的全部过程啦~还有一些小细节将在回收内存的时候进行补充

八、释放内存

  1. ThreadCache回收内存的理论

释放内存依旧是从ThreadCache开始设计,再到CentralCache、PageCache,最后还回系统。

在ThreadCache中释放内存要满足以下条件:

1. 当释放内存小于256k时将内存释放回thread cache,计算size映射自由链表桶位置i,将对象Push

到_freeLists[i]。

2. 当链表的长度过长,则回收一部分内存对象到central cache。

  1. ThreadCache回收内存代码框架

void Deallocate(void* ptr, size_t size); 释放内存接口,带size有点鸡肋,后面会修改。
void ListTooLong(FreeList& list, size_t size);链表过长的处理

接口实现

void  ListTooLong(FreeList& list, size_t size)
{
    void* start = nullptr;
    void* end = nullptr;
    将这一部分内存块从自由链表中弹出来
    list.PopRange(start, end, list.MaxSize());
    再还原到CentralCache中。
    CentralCache::GetInstance()->ReleaseToSpan(start, size);
}

回收内存
void ThreadCache::Deallocate(void* ptr, size_t size)
{
    assert(size > 0);
    size_t index = SizeClass::Index(size);
    将ptr传入到对应的桶中。
    _freelist[index].Push(ptr);
    如果当前_freelist的数量已经大于了 一次批量申请的数量时 就要还回CentralCache了
    if (_freelist[index].MaxSize() < _freelist[index].Size())
    {
        还回CentralCache的数量就是一次批量申请的数量
        传入size是为了找到在CentralCache中的index.
        ListTooLong(_freelist[index], size);
    }
}

图解:ThreadCache回收内存

  1. CentralCache回收内存的理论

首先是从ThreadCache处回收内存,ThreadCache都会释放一批内存到CentralCache中,而每一块内存都具有相应的span,不能直接将这一批内存挂到同一个span上,因为这样不利于后面还回PageCache。

当一个Span满了,就直接还给PageCache。那如何定义Span满了呢?每一个Span都具有UseCount,用于记录用了多少块内存,那么当UseCount等于0的时候,就意味着这个Span装满了,就可以还回PageCache了。

  1. CentralCache回收内存 代码框架

void Freelist::PopRange(void*& start,void*& end,size_t n)
{
    assert(n >= _size);
    start = _freelist;
    end = start;
    for (size_t i = 0; i < n - 1; i++)
    {
        end = NextObj(end);
    }
    _freelist = NextObj(end);
    _size -= n;
    NextObj(end) = nullptr;
}

这个容器是用来记录页号与Span之间的对应关系。
那这个容器是在什么时候用的呢?
答案是:在接口NewSpan的时候。就已经用上了,不过之前实现的时候,为了容易理解,没有加上。
现在大家理解这个申请内存的过程了,我们现在可以将这一步给加上了。

正好带大家回忆一下NewSpan的过程。
Span* PageCache::NewSpan(size_t k)
{
如果一次性要的内存是大于128页的,那么直接向系统申请内存。
    if (k > NPAGES - 1)
    {
        Span* span = new Span;
        void* ptr = SystemAlloc(k);
        span->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
        span->_n = k;
        _idSpanmap[span->pageId] =span;
        return span;
    }
    if (_pagelist[k].Empty())如果这一页为空,则往后面找不是空白页的地方
    {
        for (int i = k + 1; i < NPAGES; i++)
        {
            if (!_pagelist[i].Empty())
            {
                Span* nspan = _pagelist->PopFront();将这一部分内容弹出来
                Span* kspan = new Span;
设置页号,可能有的同学会好奇,这里的页号在哪呢?仔细慢慢看
                kspan->_pageId = nspan->_pageId;
页数为k
            新加入的代码:将nspan的头和尾插入到map中。
                _idSpanmap[nspan->_pageId] = nspan;
                _idSpanmap[nspan->_pageId + nspan->_n - 1] = nspan;
                kspan->_n = k;
                nspan->_pageId += k;
                nspan->_n -= k;
                遍历kspan,将kspan都加入到map中。
                for (PAGE_ID i = 0; i < kspan->_n; i++)
                {
                    _idSpanmap[kspan->_pageId + i] = kspan;
                }

                _pagelist[i - k].PushFront(nspan);
                return kspan;
            }
        }
        到这里说明_pagelist[128]都为空,这时需要向系统申请空间
        Span* bigSpan = new Span;
        void* ptr = SystemAlloc(NPAGES - 1);自己封装的向系统申请空间
        bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;计算页号的公式
        bigSpan->_n = NPAGES - 1;页号填充。
        _pagelist[bigSpan->_n].PushFront(bigSpan); 
        申请完毕后,再递归一次,此时的_pagelist[128]不为空白页,可以进行切割了。
        return NewSpan(k);
    }
    代码走到这里说明当前页不为空页,可以直接弹回Span*
    else
    {
        Span* cur = _pagelist[k].PopFront();
        for (int i = 0; i < cur->_n; i++)
        {
            _idSpanmap[cur->_pageId + i] = cur;
        }
        return cur;
    }
}

PageCache::unordered_map<PAGE_ID,Span*> _idSpanmap;

通过地址找到对应的span。
Span* PageCache::MapObjectToSpan(void* obj)
{
    通过地址找到对应的页号
    PAGE_ID id = ((PAGE_ID)obj >> PAGE_SHIFT);
    auto ret =_idSpanmap.find(id);
    if (ret != _idSpanmap.end())
    {
        return _idSpanmap[id];
    }
    return nullptr;
}

void CentralCache::ReleaseListToSpans(void* start, size_t size);

接口实现:

将ThreadCache释放的内存,还到对应的span中
void CentralCache::ReleaseToSpan(void*& start,size_t size)
{
    size_t index = SizeClass::Index(size);
    对哪一个桶操作,就需要进行加锁
    _spanlist[index]._mtx.lock();
    while (start)
    {
        void* next = NextObj(start);
        通过MaptoSpan寻找start所对应的span.
        Span* span = PageCache::GetInstance()->MaptoSpan(start);
        再挂到对应的span上
        NextObj(start) = span->_freelist;
        span->_freelist = start;
        span->_useCount--;
        当_useCount为0的时候,就需要还回PageCache了
        if (span->_useCount == 0)
        {
            span->_IsUse = false;
            对PageCache进行访问的时候,需要加锁。PageCache是只有一把锁。
            PageCache::GetInstance()->_mtx.lock();
            PageCache::GetInstance()->ReleasetoPage(span);
            PageCache::GetInstance()->_mtx.unlock();
        }
    }
    _spanlist[index]._mtx.unlock();
}
void PageCache::ReleasetoPage(Span* span)
{
    if (span->_n >= NPAGES)
    {
        void* ptr = (void*)(span->_pageId << PAGE_SHIFT);
        SystemFree(ptr);
        delete span;
        return;
    }
    //向前合并
    while (1)
    {
        PAGE_ID Prev_Id = span->_pageId - 1;
        auto prev = (Span*)_idSpanmap.get(Prev_Id);
        if (prev == nullptr)//没有找到对应的Span,不能合并
        {
            break;
        }
        if (prev->_IsUse ==true)//如果当前这个内存块被使用,则不能合并
        {
            break;
        }
        if (prev->_n + span->_n > NPAGES) //合并起来一共的页数大于128也不能合并
        {
            break;
        }
        span->_pageId = prev->_pageId;
        span->_n += prev->_n;
        _pagelist[prev->_n].Erase(prev);
        delete prev;
    }
    //向后合并
    while (1)
    {
        PAGE_ID Next_Id = span->_pageId + span->_n;
        auto ret = (Span*)_idSpanmap.get(Next_Id);
        if (ret == nullptr)
        {
            break;
        }
        //Span* next = _idSpanmap[Next_Id];
        Span* next = (Span*)_idSpanmap.get(Next_Id);
        if (next->_IsUse ==true)
        {
            break;
        }
        if (next->_n + span->_n > NPAGES)
        {
            break;
        }
        span->_n += next->_n;
        _pagelist[next->_n].Erase(next);
        delete next;
    }
    _pagelist[span->_n].PushFront(span);
    span->_IsUse = false;
    _idSpanmap.set(span->_pageId,span);
    _idSpanmap.set(span->_pageId + span->_n - 1, span);
    return;
}

图解:CentralCache回收内存

  1. PageCache回收内存理论

当CentralCache的Span满的时候,会将这个Span还给PageCache。恰好这个Span的总体大小就是X页的内存,因此只需要找到这个Span对应的页数并向前合并和向后合并

6. PageCache回收内存 代码框架

inline static void SystemFree(void* ptr)
{
#ifdef  WIN32
    VirtualFree(ptr, 0, MEM_RELEASE);
#endif //  WIN32
}
void PageCache::ReleasetoPage(Span* span)
{
    页数大于NPAGES 直接还给系统
    if (span->_n >= NPAGES)
    {
        void* ptr = (void*)(span->_pageId << PAGE_SHIFT);
        SystemFree(ptr);
        delete span;
        return;
    }
    //向前合并
    while (1)
    {
        PAGE_ID Prev_Id = span->_pageId - 1;
        auto ret = _idSpanmap.find(Prev_Id);
        //没有找到对应的Span,不能合并
        if (ret == _idSpanmap.end())
        {
            break;
        }
        Span* prev = _idSpanmap[Prev_Id];
        //如果当前这个内存块被使用,则不能合并
        if (prev->_IsUse)
        {
            break;
        }
        //合并起来一共的页数大于128也不能合并
        if (prev->_n + span->_n > NPAGES) 
        {
            break;
        }
        span->_pageId = prev->_pageId - prev->_n+1;
        span->_n += prev->_n;
        _pagelist[prev->_n].Erase(prev);
        delete prev;
    }
    //向后合并
    while (1)
    {
        PAGE_ID Next_Id = span->_pageId + span->_n;
        auto ret = _idSpanmap.find(Next_Id);
        if (ret == _idSpanmap.end())
        {
            break;
        }
        Span* next = _idSpanmap[Next_Id];
        if (next->_IsUse)
        {
            break;
        }
        if (next->_n + span->_n > NPAGES)
        {
            break;
        }
        span->_n += next->_n;
        _pagelist[next->_n].Erase(next);
        delete next;
    }
    将最后合并好的span插入到对应的桶中。
    _pagelist[span->_n].PushFront(span);
    并将它存入map中。
    _idSpanmap[span->_pageId] = span;
    _idSpanmap[span->_pageId + span->_n-1] = span;
    return;
}

九、性能测试

void BenchmarkMalloc(size_t ntimes, size_t nworks, size_t rounds)
{
    std::vector<std::thread> vthread(nworks);
    std::atomic<size_t> malloc_costtime = 0;
    std::atomic<size_t> free_costtime = 0;
    for (size_t k = 0; k < nworks; ++k)
    {
        vthread[k] = std::thread([&, k]() {
            std::vector<void*> v;
            v.reserve(ntimes);
            for (size_t j = 0; j < rounds; ++j)
            {
                size_t begin1 = clock();
                for (size_t i = 0; i < ntimes; i++)
                {
                    v.push_back(malloc(16));
                    //v.push_back(malloc((16 + i) % 8192 + 1));
                }
                size_t end1 = clock();
                size_t begin2 = clock();
                for (size_t i = 0; i < ntimes; i++)
                {
                    free(v[i]);
                }
                size_t end2 = clock();
                v.clear();
                malloc_costtime += (end1 - begin1);
                free_costtime += (end2 - begin2);
            }
            });
    }
    for (auto& t : vthread)
    {
        t.join();
    }
    printf("%u个线程并发执行%u轮次,每轮次concurrent alloc %u次: 花费:%u ms\n",
        nworks, rounds, ntimes, malloc_costtime.load());
    printf("%u个线程并发执行%u轮次,每轮次concurrent dealloc %u次: 花费:%u ms\n",
        nworks, rounds, ntimes, free_costtime.load());
    printf("%u个线程并发concurrent alloc&dealloc %u次,总计花费:%u ms\n",
        nworks, nworks * rounds * ntimes, malloc_costtime.load() + free_costtime.load());
}

// 单轮次申请释放次数 线程数 轮次
void BenchmarkConcurrentMalloc(size_t ntimes, size_t nworks, size_t rounds)
{
    std::vector<std::thread> vthread(nworks);
    std::atomic<size_t> malloc_costtime = 0;
    std::atomic<size_t> free_costtime = 0;
    for (size_t k = 0; k < nworks; ++k)
    {
        vthread[k] = std::thread([&]() {
            std::vector<void*> v;
            v.reserve(ntimes);
            for (size_t j = 0; j < rounds; ++j)
            {
                size_t begin1 = clock();
                for (size_t i = 0; i < ntimes; i++)
                {
                    v.push_back(ConcurrentAlloc(16));
                    //v.push_back(ConcurrentAlloc((16 + i) % 8192 + 1));
                }
                size_t end1 = clock();
                size_t begin2 = clock();
                for (size_t i = 0; i < ntimes; i++)
                {
                    ConcurrentFree(v[i]);
                }
                size_t end2 = clock();
                v.clear();
                malloc_costtime += (end1 - begin1);
                free_costtime += (end2 - begin2);
            }
            });
    }
    for (auto& t : vthread)
    {
        t.join();
    }
    printf("%u个线程并发执行%u轮次,每轮次concurrent alloc %u次: 花费:%u ms\n",
        nworks, rounds, ntimes, malloc_costtime.load());
    printf("%u个线程并发执行%u轮次,每轮次concurrent dealloc %u次: 花费:%u ms\n",
        nworks, rounds, ntimes, free_costtime.load());
    printf("%u个线程并发concurrent alloc&dealloc %u次,总计花费:%u ms\n",
        nworks, nworks * rounds * ntimes, malloc_costtime.load() + free_costtime.load());
}

int main()
{
    size_t n = 100000;
    std::cout << "==========================================================" <<std::endl;
    BenchmarkConcurrentMalloc(n, 6, 10);
    std::cout << std::endl << std::endl;
    BenchmarkMalloc(n, 6, 10);
    std::cout << "==========================================================" <<std::endl;
    return 0;
}

十、性能优化

1.将PageCache中的unordered_map替换

我们不再使用unordered_map,进行保存PAGE_ID 与 Span*。而是选择使用tcmalloc中的基数树来进行替代。

基数树一共有三种。

第一种:

template <int BITS> //非类型模板参数,当做常数用
class TCMalloc_PageMap1 {
    //BITS = 32 - PAGE_SHIFT 或者 64 - PAGE_SHIFT
    //其本质也是一个哈希表,不过长度是 1<<19 ≈ 52W 数组元素为 Span*
private:
    static const int LENGTH = 1 << BITS; //LENGTH是存储页号最多需要位。
    void** array_;
public:
    typedef uintptr_t Number;//uintptr_t 是unsigned int 
    explicit TCMalloc_PageMap1() {
        size_t size = sizeof(void*) << BITS;//计算一层的大小。
        size_t alignSize = SizeClass::_RoundUp(size,1<<PAGE_SHIFT);//向上取
        array_ = (void**)SystemAlloc(alignSize >> PAGE_SHIFT);
        //SystemAlloc申请内存是按页数申请的。
        memset(array_, 0, sizeof(void*) << BITS);
    }
    // Return the current value for KEY. Returns NULL if not yet set,
    // or if k is out of range.
    void* get(Number k) const { //读    通过k找到void*
        if ((k >> BITS) > 0) {
            return NULL;
        }
        return array_[k];
    }
    // REQUIRES "k" is in range "[0,2^BITS-1]".
    // REQUIRES "k" has been ensured before.
    // Sets the value 'v' for key 'k'.
    void set(Number k, void* v) { //写
        array_[k] = v;
    }
};

物理结构:

接口set:用于存储PAGE_ID,Span*之间的对应关系。

接口get:通过PAGE_ID,查找Span*。

第二种:

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;// 19-5 =14
    static const int LEAF_LENGTH = 1 << LEAF_BITS; // 16384个
    // Leaf node
    struct Leaf {
        void* values[LEAF_LENGTH];
    };
    Leaf* root_[ROOT_LENGTH]; // Pointers to 32 child nodes
public:
    typedef uintptr_t Number;
    //explicit TCMalloc_PageMap2(void* (*allocator)(size_t)) {
    explicit TCMalloc_PageMap2() {
        //allocator_ = allocator;
        memset(root_, 0, sizeof(root_));
        PreallocateMoreMemory();
    }
    void* get(Number k) const {
        const Number i1 = k >> LEAF_BITS;//第一层
        const Number i2 = k & (LEAF_LENGTH - 1); //第二层
        if ((k >> BITS) > 0 || root_[i1] == NULL) {
            return NULL;
        }
        return root_[i1]->values[i2];
    }
    void set(Number k, void* v) {
        const Number i1 = k >> LEAF_BITS;
        const Number i2 = k & (LEAF_LENGTH - 1);
        ASSERT(i1 < ROOT_LENGTH);
        root_[i1]->values[i2] = v;
    }
    bool Ensure(Number start, size_t n) {
        for (Number key = start; key <= start + n - 1;) {
            const Number i1 = key >> LEAF_BITS;
            // Check for overflow
            if (i1 >= ROOT_LENGTH)
                return false;
            // Make 2nd level node if necessary
            if (root_[i1] == NULL) {
                {
                    static ObjectPool<Leaf> LeafPool;
                    Leaf* leaf = (Leaf*)LeafPool.New();
                    memset(leaf, 0, sizeof(*leaf));
                    root_[i1] = leaf;
                }
            }
            key = ((key >> LEAF_BITS) + 1) << LEAF_BITS;
        }
        return true;
    }
    void PreallocateMoreMemory() {
        Ensure(0, 1 << BITS);
    }
};

物理结构:以32位举例

set:先确定槽位,再到槽位中去找对应的Span*。

get:同上述。

2.将所有的new都换成定长内存池。

在PageCache中定义ObjectPool<Span> _spanPool

new都切换成 _spanPool.New()

十一、总结

该项目将申请内存分为了三个层,并采取了基数树和定长内存池进行优化。项目总体难度适中,代码量不多,但理解起来比较难,希望大家都能够自己敲一遍代码!还有就是调试的时候比较麻烦,需要大家耐心地去调试。如果有不懂或者错误的地方,希望大家私信我,或者评论,谢谢大家!

十二、Gitee源码

tcmalloc源码:tcmalloc: TCMalloc (google-perftools) 是用于优化C++写的多线程应用,比glibc 2.3的malloc快。这个模块可以用来让MySQL在高并发下内存占用更加稳定。 (gitee.com)

本博客代码:https://gitee.com/www_spj_com/high-concurrency-memory-pool

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值