内存“银行”(一)

简介: 内存“银行”

项目介绍

       本项目实现的是一个内存银行,它的原型是Google的一个开源项目tcmalloc,tcmalloc全称Thread-Caching Malloc,即线程缓存的malloc,实现了高效的多线程内存管理,用于替换系统的内存分配相关函数malloc和free。

有了malloc为什么需要tcmalloc?

TCMalloc 是用于优化C++写的多线程应用,比Linux的malloc快。这个模块可以用来让MySQL在高并发下内存占用更加稳定比malloc在多线程的情况下更快

项目实现后的效果

在项目完成后,拿我们实现的tcmalloc去和原先的malloc对比。

申请固定内存大小时,我创建了4个线程,分别执行10轮操作,每轮n申请释放1万次,也就是一共进行了40万次的申请释放,结果是malloc用了31ms左右,而tcmlloc只用了11ms左右,就是快了3倍

当我尝试申请不同内存时,同样的,4个线程,执行10轮,1轮1万次,共40万次申请释放操作,这次,malloc的时间用了1023ms,而tcmalloc只用了113ms,倍数来到了10倍

也就是说,理想状况下,在未来的多线程开发环境下申请不同的内存,使用malloc函数如果要用10s才能完成内存的申请和释放,我只要用1s。

内存"银行"整体框架设计

内存"银行"主要由以下三个部分构成:

thread cache: 线程缓存是每个线程独有的,用于小于等于256KB的内存分配,每个线程独享一个thread cache。

central cache: 中心缓存是所有线程所共享的,当thread cache需要内存时会按需从central cache中获取内存,而当thread cache中的内存满足一定条件时,central cache也会在合适的时机对其进行回收。

page cache: 页缓存中存储的内存是以页为单位进行存储及分配的,当central cache需要内存时,page cache会分配出一定数量的页分配给central cache,而当central cache中的内存满足一定条件时,page cache也会在合适的时机对其进行回收,并将回收的内存尽可能的进行合并,组成更大的连续内存块,缓解内存碎片的问题。

进一步说明:

  每个线程都有一个属于自己的thread cache,也就意味着线程在thread cache申请内存时是不需要加锁的,而一次性申请大于256KB内存的情况是很少的,因此大部分情况下申请内存时都是无锁的,这也就是这个高并发内存池高效的地方。

  每个线程的thread cache会根据自己的情况向central cache申请或归还内存,这就避免了出现单个线程的thread cache占用太多内存,而其余thread cache出现内存吃紧的问题。

  多线程的thread cache可能会同时找central cache申请内存,此时就会涉及线程安全的问题,因此在访问central cache时是需要加锁的,但central cache实际上是一个哈希桶的结构,只有当多个线程同时访问同一个桶时才需要加锁,所以这里的锁竞争也不会很激烈。

各个部分的主要作用

  thread cache主要解决锁竞争的问题,每个线程独享自己的thread cache,当自己的thread cache中有内存时该线程不会去和其他线程进行竞争,每个线程只要在自己的thread cache申请内存就行了。

  central cache主要起到一个居中调度的作用,每个线程的thread cache需要内存时从central cache获取,而当thread cache的内存多了就会将内存还给central cache,其作用类似于一个中枢,因此取名为中心缓存。

  page cache就负责提供以页为单位的大块内存,当central cache需要内存时就会去向page cache申请,而当page cache没有内存了就会直接去找系统,也就是直接去堆上按页申请内存块。

 

threadcache

threadcache整体设计

       定长内存池只支持固定大小内存块的申请释放,因此定长内存池中只需要一个自由链表管理释放回来的内存块。现在我们要支持申请和释放不同大小的内存块,那么我们就需要多个自由链表来管理释放回来的内存块,因此thread cache实际上一个哈希桶结构,每个桶中存放的都是一个自由链表。

  thread cache支持小于等于256KB内存的申请,如果我们将每种字节数的内存块都用一个自由链表进行管理的话,那么此时我们就需要20多万个自由链表,光是存储这些自由链表的头指针就需要消耗大量内存,这显然是得不偿失的。

  这时我们可以选择做一些平衡的牺牲,让这些字节数按照某种规则进行对齐,例如我们让这些字节数都按照8字节进行向上对齐,那么thread cache的结构就是下面这样的,此时当线程申请1~8字节的内存时会直接给出8字节,而当线程申请9~16字节的内存时会直接给出16字节,以此类推。

  因此当线程要申请某一大小的内存块时,就需要经过某种计算得到对齐后的字节数,进而找到对应的哈希桶,如果该哈希桶中的自由链表中有内存块,那就从自由链表中头删一个内存块进行返回;如果该自由链表已经为空了,那么就需要向下一层的central cache进行获取了。

  但此时由于对齐的原因,就可能会产生一些碎片化的内存无法被利用,比如线程只申请了6字节的内存,而thread cache却直接给了8字节的内存,这多给出的2字节就无法被利用,导致了一定程度的空间浪费,这些因为某些对齐原因导致无法被利用的内存,就是内存碎片中的内部碎片。

 

// 管理切分好的小对象的自由链表
class FreeList
{
public:
  //将释放的对象头插到自由链表
  void Push(void* obj)
  {
    assert(obj);
    //头插
    NextObj(obj) = _freeList;
    _freeList = obj;
  }
  //从自由链表头部获取一个对象
  void* Pop()
  {
    assert(_freeList);
    //头删
    void* obj = _freeList;
    _freeList = NextObj(_freeList);
    return obj;
  }
private:
  void* _freeList = nullptr; //自由链表
};

  因此thread cache实际就是一个数组,数组中存储的就是一个个的自由链表,至于这个数组中到底存储了多少个自由链表,就需要看我们在进行字节数对齐时具体用的是什么映射对齐规则了。

threadcache哈希桶映射对齐规则

       thread cache实际就是一个数组,数组中存储的就是一个个的自由链表,至于这个数组中到底存储了多少个自由链表,就需要看我们在进行字节数对齐时具体用的是什么映射对齐规则了。

如何进行对齐?

  上面已经说了,不是每个字节数都对应一个自由链表,这样开销太大了,因此我们需要制定一个合适的映射对齐规则。

  首先,这些内存块是会被链接到自由链表上的,因此一开始肯定是按8字节进行对齐是最合适的,因为我们必须保证这些内存块,无论是在32位平台下还是64位平台下,都至少能够存储得下一个指针。

  但如果所有的字节数都按照8字节进行对齐的话,那么我们就需要建立256 × 1024 ÷ 8 = 32768个桶,这个数量还是比较多的,实际上我们可以让不同范围的字节数按照不同的对齐数进行对齐,具体对齐方式如下:

空间浪费率(内碎片浪费率)

  虽然对齐产生的内碎片会引起一定程度的空间浪费,但按照上面的对齐规则,我们可以将浪费率控制到百分之十左右。需要说明的是,1~128这个区间我们不做讨论,因为1字节就算是对齐到2字节也有百分之五十的浪费率,这里我们就从第二个区间开始进行计算。

浪费率 = 浪费的字节数 / 对齐后的字节数

  根据上面的公式,我们要得到某个区间的最大浪费率,就应该让分子取到最大,让分母取到最小。比如129~1024这个区间,该区域的对齐数是16,那么最大浪费的字节数就是15,而最小对齐后的字节数就是这个区间内的前16个数所对齐到的字节数,也就是144,那么该区间的最大浪费率也就是15 ÷ 144 ≈ 10.42 %。同样的道理,后面两个区间的最大浪费率分别是127 ÷ 1152 ≈ 11.02 % 和1023 ÷ 9216 ≈ 11.10 %

       在获取某一字节数向上对齐后的字节数时,可以先判断该字节数属于哪一个区间,然后再通过调用一个子函数进行进一步处理。

// 获取向上对齐后的字节数
static inline size_t RoundUp(size_t bytes)
{
  if (bytes <= 128)
  {
    return _RoundUp(bytes, 8);
  }
  else if (bytes <= 1024)
  {
    return _RoundUp(bytes, 16);
  }
  else if (bytes <= 8 * 1024)
  {
    return _RoundUp(bytes, 128);
  }
  else if (bytes <= 64 * 1024)
  {
    return _RoundUp(bytes, 1024);
  }
  else if (bytes <= 256 * 1024)
  {
    return _RoundUp(bytes, 8 * 1024);
  }
  else
  {
    assert(false);
    return -1;
  }
}

       此时我们就需要编写一个子函数,该子函数需要通过对齐数计算出某一字节数对齐后的字节数,最容易想到的就是下面这种写法。

static inline size_t _RoundUp(size_t bytes, size_t alignNum)
{
  size_t alignSize = 0;
  if (bytes%alignNum != 0)
  {
    alignSize = (bytes / alignNum + 1)*alignNum;
  }
  else
  {
    alignSize = bytes;
  }
  return alignSize;
}

       在获取某一字节数对应的哈希桶下标时,也是先判断该字节数属于哪一个区间,然后再通过调用一个子函数进行进一步处理。

// 获取对应哈希桶的下标
static inline size_t Index(size_t bytes)
{
  //每个区间有多少个自由链表
  static size_t groupArray[4] = { 16, 56, 56, 56 };
  if (bytes <= 128)
  {
    return _Index(bytes, 3);
  }
  else if (bytes <= 1024)
  {
    return _Index(bytes - 128, 4) + groupArray[0];
  }
  else if (bytes <= 8 * 1024)
  {
    return _Index(bytes - 1024, 7) + groupArray[0] + groupArray[1];
  }
  else if (bytes <= 64 * 1024)
  {
    return _Index(bytes - 8 * 1024, 10) + groupArray[0] + groupArray[1] + groupArray[2];
  }
  else if (bytes <= 256 * 1024)
  {
    return _Index(bytes - 64 * 1024, 13) + groupArray[0] + groupArray[1] + groupArray[2] + groupArray[3];
  }
  else
  {
    assert(false);
    return -1;
  }
}

       此时我们需要编写一个子函数来继续进行处理,容易想到的就是根据对齐数来计算某一字节数对应的下标。

// 一般写法
static inline size_t _Index(size_t bytes, size_t alignNum)
{
  size_t index = 0;
  if (bytes%alignNum != 0)
  {
    index = bytes / alignNum;
  }
  else
  {
    index = bytes / alignNum - 1;
  }
  return index;
}

ThreadCache类

       按照上述的对齐规则,thread cache中桶的个数,也就是自由链表的个数是208,以及thread cache允许申请的最大内存大小256KB,我们可以将这些数据按照如下方式进行定义。

//小于等于MAX_BYTES,就找thread cache申请
//大于MAX_BYTES,就直接找page cache或者系统堆申请
static const size_t MAX_BYTES = 256 * 1024;
//thread cache和central cache自由链表哈希桶的表大小
static const size_t NFREELISTS = 208;

       现在就可以对ThreadCache类进行定义了,thread cache就是一个存储208个自由链表的数组,目前thread cache就先提供一个Allocate函数用于申请对象就行了,后面需要时再进行增加。

class ThreadCache
{
public:
    //申请内存对象
    void* Allocate(size_t size);
private:
    FreeList _freeLists[NFREELISTS]; //哈希桶
};

       在thread cache申请对象时,通过所给字节数计算出对应的哈希桶下标,如果桶中自由链表不为空,则从该自由链表中取出一个对象进行返回即可;但如果此时自由链表为空,那么我们就需要从central cache进行获取了,这里的FetchFromCentralCache函数也是thread cache类中的一个成员函数,在后面再进行具体实现。

//申请内存对象
void* ThreadCache::Allocate(size_t size)
{
    assert(size <= MAX_BYTES);
    size_t alignSize = SizeClass::RoundUp(size);
    size_t index = SizeClass::Index(size);
    if (!_freeLists[index].Empty())
    {
        return _freeLists[index].Pop();
    }
    else
    {
        return FetchFromCentralCache(index, alignSize);
    }
}

 

threadcacheTLS无锁访问

       每个线程都有一个自己独享的thread cache,那应该如何创建这个thread cache呢?我们不能将这个thread cache创建为全局的,因为全局变量是所有线程共享的,这样就不可避免的需要锁来控制,增加了控制成本和代码复杂度。

  要实现每个线程无锁的访问属于自己的thread cache,我们需要用到线程局部存储TLS(Thread Local Storage),这是一种变量的存储方法,使用该存储方法的变量在它所在的线程是全局可访问的,但是不能被其他线程访问到,这样就保持了数据的线程独立性。

 //TLS - Thread Local Storage
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;

       但不是每个线程被创建时就立马有了属于自己的thread cache,而是当该线程调用相关申请内存的接口时才会创建自己的thread cache,因此在申请内存的函数中会包含以下逻辑。

//通过TLS,每个线程无锁的获取自己专属的ThreadCache对象
if (pTLSThreadCache == nullptr)
{
    pTLSThreadCache = new ThreadCache;
}

       

centralcache

centralcache整体设计

       当线程申请某一大小的内存时,如果thread cache中对应的自由链表不为空,那么直接取出一个内存块进行返回即可,但如果此时该自由链表为空,那么这时thread cache就需要向central cache申请内存了。

       central cache的结构与thread cache是一样的,它们都是哈希桶的结构,并且它们遵循的对齐映射规则都是一样的。这样做的好处就是,当thread cache的某个桶中没有内存了,就可以直接到central cache中对应的哈希桶里去取内存就行了。

central cache与thread cache的不同之处

  central cache与thread cache有两个明显不同的地方,首先,thread cache是每个线程独享的,而central cache是所有线程共享的,因为每个线程的thread cache没有内存了都会去找central cache,因此在访问central cache时是需要加锁的。

  但central cache在加锁时并不是将整个central cache全部锁上了,central cache在加锁时用的是桶锁,也就是说每个桶都有一个锁。此时只有当多个线程同时访问central cache的同一个桶时才会存在锁竞争,如果是多个线程同时访问central cache的不同桶就不会存在锁竞争。

  central cache与thread cache的第二个不同之处就是,thread cache的每个桶中挂的是一个个切好的内存块,而central cache的每个桶中挂的是一个个的span。

  每个span管理的都是一个以页为单位的大块内存,每个桶里面的若干span是按照双链表的形式链接起来的,并且每个span里面还有一个自由链表,这个自由链表里面挂的就是一个个切好了的内存块,根据其所在的哈希桶这些内存块被切成了对应的大小。

 

centralcache结构设计

页号的类型?

  每个程序运行起来后都有自己的进程地址空间,在32位平台下,进程地址空间的大小是232;而在64位平台下,进程地址空间的大小就是2^64。

  页的大小一般是4K或者8K,我们以8K为例。在32位平台下,进程地址空间就可以被分成 2^32 ÷ 2^13 = 2 ^19 个页;在64位平台下,进程地址空间就可以被分成 2^64 ÷ 2^13 = 2^51 个页。页号本质与地址是一样的,它们都是一个编号,只不过地址是以一个字节为一个单位,而页是以多个字节为一个单位。

  由于页号在64位平台下的取值范围是 [0 , 2^51 ) ,因此我们不能简单的用一个无符号整型来存储页号,这时我们需要借助条件编译来解决这个问题。

#ifdef _WIN64
    typedef unsigned long long PAGE_ID;
#elif _WIN32
    typedef size_t PAGE_ID;
#else
    //linux
#endif

       需要注意的是,在32位下,_WIN32有定义,_WIN64没有定义;而在64位下,_WIN32和_WIN64都有定义。因此在条件编译时,我们应该先判断_WIN64是否有定义,再判断_WIN32是否有定义。

span的结构

central cache的每个桶里挂的是一个个的span,span是一个管理以页为单位的大块内存,span的结构如下:

//管理以页为单位的大块内存
struct Span
{
    PAGE_ID _pageId = 0;        //大块内存起始页的页号
    size_t _n = 0;              //页的数量
    Span* _next = nullptr;      //双链表结构
    Span* _prev = nullptr;
    size_t _useCount = 0;       //切好的小块内存,被分配给thread cache的计数
    void* _freeList = nullptr;  //切好的小块内存的自由链表
};

       对于span管理的以页为单位的大块内存,我们需要知道这块内存具体在哪一个位置,便于之后page cache进行前后页的合并,因此span结构当中会记录所管理大块内存起始页的页号。

  至于每一个span管理的到底是多少个页,这并不是固定的,需要根据多方面的因素来控制,因此span结构当中有一个_n成员,该成员就代表着该span管理的页的数量。

  此外,每个span管理的大块内存,都会被切成相应大小的内存块挂到当前span的自由链表中,比如8Byte哈希桶中的span,会被切成一个个8Byte大小的内存块挂到当前span的自由链表中,因此span结构中需要存储切好的小块内存的自由链表。

  span结构当中的_useCount成员记录的就是,当前span中切好的小块内存,被分配给thread cache的计数,当某个span的_useCount计数变为0时,代表当前span切出去的内存块对象全部还回来了,此时central cache就可以将这个span再还给page cache。

  每个桶当中的span是以双链表的形式组织起来的,当我们需要将某个span归还给page cache时,就可以很方便的将该span从双链表结构中移出。如果用单链表结构的话就比较麻烦了,因为单链表在删除时,需要知道当前结点的前一个结点。

 

双链表结构

  根据上面的描述,central cache的每个哈希桶里面存储的都是一个双链表结构,对于该双链表结构我们可以对其进行封装。

//带头双向循环链表
class SpanList
{
public:
    SpanList()
    {
        _head = new Span;
        _head->_next = _head;
        _head->_prev = _head;
    }
    void Insert(Span* pos, Span* newSpan)
    {
        assert(pos);
        assert(newSpan);
        Span* prev = pos->_prev;
        prev->_next = newSpan;
        newSpan->_prev = prev;
        newSpan->_next = pos;
        pos->_prev = newSpan;
    }
    void Erase(Span* pos)
    {
        assert(pos);
        assert(pos != _head); //不能删除哨兵位的头结点
        Span* prev = pos->_prev;
        Span* next = pos->_next;
        prev->_next = next;
        next->_prev = prev;
    }
private:
    Span* _head;
public:
    std::mutex _mtx; //桶锁
};

       需要注意的是,从双链表删除的span会还给下一层的page cache,相当于只是把这个span从双链表中移除,因此不需要对删除的span进行delete操作。

central cache的结构

  central cache的映射规则和thread cache是一样的,因此central cache里面哈希桶的个数也是208,但central cache每个哈希桶中存储就是我们上面定义的双链表结构。

class CentralCache
{
public:
    //...
private:
    SpanList _spanLists[NFREELISTS];
};

       central cache和thread cache的映射规则一样,有一个好处就是,当thread cache的某个桶没有内存了,就可以直接去central cache对应的哈希桶进行申请就行了。

centralcache核心实现

central cache的实现方式

  每个线程都有一个属于自己的thread cache,我们是用TLS来实现每个线程无锁的访问属于自己的thread cache的。而central cache和page cache在整个进程中只有一个,对于这种只能创建一个对象的类,我们可以将其设置为单例模式。

  单例模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。单例模式又分为饿汉模式和懒汉模式,懒汉模式相对较复杂,我们这里使用饿汉模式就足够了。

//单例模式
class CentralCache
{
public:
    //提供一个全局访问点
    static CentralCache* GetInstance()
    {
        return &_sInst;
    }
private:
    SpanList _spanLists[NFREELISTS];
private:
    CentralCache() //构造函数私有
    {}
    CentralCache(const CentralCache&) = delete; //防拷贝
    static CentralCache _sInst;
};

       为了保证CentralCache类只能创建一个对象,我们需要将central cache的构造函数和拷贝构造函数设置为私有,或者在C++11中也可以在函数声明的后面加上=delete进行修饰。

  CentralCache类当中还需要有一个CentralCache类型的静态的成员变量,当程序运行起来后我们就立马创建该对象,在此后的程序中就只有这一个单例了。

CentralCache CentralCache::_sInst;

       最后central cache还需要提供一个公有的成员函数,用于获取该对象,此时在整个进程中就只会有一个central cache对象了。

慢开始反馈调节算法

  当thread cache向central cache申请内存时,central cache应该给出多少个对象呢?这是一个值得思考的问题,如果central cache给的太少,那么thread cache在短时间内用完了又会来申请;但如果一次性给的太多了,可能thread cache用不完也就浪费了。

  鉴于此,我们这里采用了一个慢开始反馈调节算法。当thread cache向central cache申请内存时,如果申请的是较小的对象,那么可以多给一点,但如果申请的是较大的对象,就可以少给一点。

  通过下面这个函数,我们就可以根据所需申请的对象的大小计算出具体给出的对象个数,并且可以将给出的对象个数控制到2~512个之间。也就是说,就算thread cache要申请的对象再小,我最多一次性给出512个对象;就算thread cache要申请的对象再大,我至少一次性给出2个对象。

//管理对齐和映射等关系
class SizeClass
{
public:
    //thread cache一次从central cache获取对象的上限
    static size_t NumMoveSize(size_t size)
    {
        assert(size > 0);
        //对象越小,计算出的上限越高
        //对象越大,计算出的上限越低
        int num = MAX_BYTES / size;
        if (num < 2)
            num = 2;
        if (num > 512)
            num = 512;
        return num;
    }
};

       但就算申请的是小对象,一次性给出512个也是比较多的,基于这个原因,我们可以在FreeList结构中增加一个叫做_maxSize的成员变量,该变量的初始值设置为1,并且提供一个公有成员函数用于获取这个变量。也就是说,现在thread cache中的每个自由链表都会有一个自己的_maxSize。

// 管理切分好的小对象的自由链表
class FreeList
{
public:
    size_t& MaxSize()
    {
        return _maxSize;
    }
private:
    void* _freeList = nullptr; //自由链表
    size_t _maxSize = 1;
};

       此时当thread cache申请对象时,我们会比较_maxSize和计算得出的值,取出其中的较小值作为本次申请对象的个数。此外,如果本次采用的是_maxSize的值,那么还会将thread cache中该自由链表的_maxSize的值进行加一。

  因此,thread cache第一次向central cache申请某大小的对象时,申请到的都是一个,但下一次thread cache再向central cache申请同样大小的对象时,因为该自由链表中的_maxSize增加了,最终就会申请到两个。直到该自由链表中_maxSize的值,增长到超过计算出的值后就不会继续增长了,此后申请到的对象个数就是计算出的个数。(这有点像网络中拥塞控制的机制)

 

从中心缓存获取对象

  每次thread cache向central cache申请对象时,我们先通过慢开始反馈调节算法计算出本次应该申请的对象的个数,然后再向central cache进行申请。

  如果thread cache最终申请到对象的个数就是一个,那么直接将该对象返回即可。为什么需要返回一个申请到的对象呢?因为thread cache要向central cache申请对象,其实由于某个线程向thread cache申请对象但thread cache当中没有,这才导致thread cache要向central cache申请对象。因此central cache将对象返回给thread cache后,thread cache会再将该对象返回给申请对象的线程。

  但如果thread cache最终申请到的是多个对象,那么除了将第一个对象返回之外,还需要将剩下的对象挂到thread cache对应的哈希桶当中。

//从中心缓存获取对象
void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{
    //慢开始反馈调节算法
    //1、最开始不会一次向central cache一次批量要太多,因为要太多了可能用不完
    //2、如果你不断有size大小的内存需求,那么batchNum就会不断增长,直到上限
    size_t batchNum = std::min(_freeLists[index].MaxSize(), SizeClass::NumMoveSize(size));
    if (batchNum == _freeLists[index].MaxSize())
    {
        _freeLists[index].MaxSize() += 1;
    }
    void* start = nullptr;
    void* end = nullptr;
    size_t actualNum = CentralCache::GetInstance()->FetchRangeObj(start, end, batchNum, size);
    assert(actualNum >= 1); //至少有一个
    if (actualNum == 1) //申请到对象的个数是一个,则直接将这一个对象返回即可
    {
        assert(start == end);
        return start;
    }
    else //申请到对象的个数是多个,还需要将剩下的对象挂到thread cache中对应的哈希桶中
    {
        _freeLists[index].PushRange(NextObj(start), end);
        return start;
    }
}

从中心缓存获取一定数量的对象

  这里我们要从central cache获取n个指定大小的对象,这些对象肯定都是从central cache对应哈希桶的某个span中取出来的,因此取出来的这n个对象是链接在一起的,我们只需要得到这段链表的头和尾即可,这里可以采用输出型参数进行获取。

//从central cache获取一定数量的对象给thread cache
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t n, size_t size)
{
    size_t index = SizeClass::Index(size);
    _spanLists[index]._mtx.lock(); //加锁
    //在对应哈希桶中获取一个非空的span
    Span* span = GetOneSpan(_spanLists[index], size);
    assert(span); //span不为空
    assert(span->_freeList); //span当中的自由链表也不为空
    //从span中获取n个对象
    //如果不够n个,有多少拿多少
    start = span->_freeList;
    end = span->_freeList;
    size_t actualNum = 1;
    while (NextObj(end)&&n - 1)
    {
        end = NextObj(end);
        actualNum++;
        n--;
    }
    span->_freeList = NextObj(end); //取完后剩下的对象继续放到自由链表
    NextObj(end) = nullptr; //取出的一段链表的表尾置空
    span->_useCount += actualNum; //更新被分配给thread cache的计数
    _spanLists[index]._mtx.unlock(); //解锁
    return actualNum;
}

       由于central cache是所有线程共享的,所以我们在访问central cache中的哈希桶时,需要先给对应的哈希桶加上桶锁,在获取到对象后再将桶锁解掉。

  在向central cache获取对象时,先是在central cache对应的哈希桶中获取到一个非空的span,然后从这个span的自由链表中取出n个对象即可,但可能这个非空的span的自由链表当中对象的个数不足n个,这时该自由链表当中有多少个对象就给多少就行了。

  也就是说,thread cache实际从central cache获得的对象的个数可能与我们传入的n值是不一样的,因此我们需要统计本次申请过程中,实际thread cache获取到的对象个数,然后根据该值及时更新这个span中的小对象被分配给thread cache的计数。

  需要注意的是,虽然我们实际申请到对象的个数可能比n要小,但这并不会产生任何影响。因为thread cache的本意就是向central cache申请一个对象,我们之所以要一次多申请一些对象,是因为这样一来下次线程再申请相同大小的对象时就可以直接在thread cache里面获取了,而不用再向central cache申请对象。

插入一段范围的对象到自由链表

  此外,如果thread cache最终从central cache获取到的对象个数是大于一的,那么我们还需要将剩下的对象插入到thread cache中对应的哈希桶中,为了能让自由链表支持插入一段范围的对象,我们还需要在FreeList类中增加一个对应的成员函数。

//管理切分好的小对象的自由链表
class FreeList
{
public:
    //插入一段范围的对象到自由链表
    void PushRange(void* start, void* end)
    {
        assert(start);
        assert(end);
        //头插
        NextObj(end) = _freeList;
        _freeList = start;
    }
private:
    void* _freeList = nullptr; //自由链表
    size_t _maxSize = 1;
};


相关文章
|
9月前
|
存储 缓存 安全
内存“银行”(三)
内存“银行”(三)
|
9月前
|
存储 缓存 算法
内存“银行”(二)
内存“银行”(二)
|
3天前
|
存储
浮点数在内存中的存储
浮点数在内存中的存储
25 0
|
3天前
|
存储
数据在内存中的存储之整数存储
数据在内存中的存储之整数存储
21 0
|
3天前
|
存储 C语言
C语言第二十九弹---浮点数在内存中的存储
C语言第二十九弹---浮点数在内存中的存储
|
2天前
|
存储 小程序 编译器
数据在内存中的存储(探索内存的秘密)
数据在内存中的存储(探索内存的秘密)
9 0
|
3天前
|
存储 监控 NoSQL
Redis处理大量数据主要依赖于其内存存储结构、高效的数据结构和算法,以及一系列的优化策略
【5月更文挑战第15天】Redis处理大量数据依赖内存存储、高效数据结构和优化策略。选择合适的数据结构、利用批量操作减少网络开销、控制批量大小、使用Redis Cluster进行分布式存储、优化内存使用及监控调优是关键。通过这些方法,Redis能有效处理大量数据并保持高性能。
22 0
|
1天前
|
存储 算法 关系型数据库
实时计算 Flink版产品使用合集之在Flink Stream API中,可以在任务启动时初始化一些静态的参数并将其存储在内存中吗
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStream API、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。
15 4
|
3天前
|
存储 编译器 程序员
C语言:数据在内存中的存储
C语言:数据在内存中的存储
15 2
|
3天前
|
存储
整数和浮点数在内存中存储
整数的2进制表⽰⽅法有三种,即原码、反码和补码。
18 0