高并发内存池的整体框架以及thread cache

  1. 高并发内存池的整体框架设计

现代很多的开发环境都是多核多线程,在申请内存的场景下,必然存在激烈的锁竞争问题。malloc本身 其实已经很优秀,那么我们项目的原型tcmalloc就是在多线程高并发的场景下更胜一筹,所以这次我们 实现的内存池需要考虑以下几方面的问题。 1. 性能问题。 2. 多线程环境下,锁竞争问题。 3. 内存碎片问题。 concurrent memory pool主要由以下3个部分构成: 1. thread cache:线程缓存是每个线程独有的,有多少线程就有多少thread cache管理内存结构,用于小于256KB的内存的分配,线程从这里申请内 存不需要加锁,每个线程独享一个cache,自己的thread cache有内存就在自己这里申请,这也就是这个并发线程池高效的地方。

2. central cache:中心缓存是所有线程所共享,thread cache是按需从central cache中获取的对 象。central cache合适的时机回收thread cache中的对象,避免一个线程占用了太多的内存,而 其他线程的内存吃紧,达到内存分配在多个线程中更均衡的按需调度的目的。”居中调度“central cache是存 在竞争的,所以从这里取内存对象是需要加锁,首先这里用的是桶锁,(thread1和2访问同一个哈希桶时)其次只有thread cache的 没有内存对象时才会找central cache,所以这里竞争不会很激烈。

3. page cache:页缓存是在central cache缓存上面的一层缓存,存储的内存是以页为单位存储及分 配的,central cache没有内存对象时,从page cache分配出一定数量的page,并切割成定长大小 的小块内存,分配给central cache。当一个span的几个跨度页的对象都回收以后,page cache 会回收central cache满足条件的span对象,并且合并相邻的页(页号探测,前后连续就合并),组成更大的页,缓解内存碎片 的问题。

也就是thread cache找central cache(切小后),central cache找page cache(切小后) ,page cache(切小后) 找系统(brk map...),当然也会有回收机制

2.高并发内存池——thread cache

thread cache是哈希桶结构,每个桶是一个按桶位置映射大小的内存块对象的自由链表。每个线程都会 有一个thread cache对象,这样每个线程在这里获取对象和释放对象时是无锁的。

内碎片:用不上在申请的空间里面,由于内存对齐的需要,小空间里用不了

外碎片:连续空间被分解,只有部分换回来,空间够但不连续

申请内存:

1. 当内存申请size<=256KB,先获取到线程本地存储的thread cache对象,计算size映射的哈希桶自 由链表下标i。 2. 如果自由链表_freeLists[i]中有对象,则直接Pop一个内存对象返回。 3. 如果_freeLists[i]中没有对象时,则批量从central cache中获取一定数量的对象,插入到自由链表 并返回一个对象。

释放内存:

1. 当释放内存小于256k时将内存释放回thread cache,计算size映射自由链表桶位置i,将对象Push 到_freeLists[i]。 2. 当链表的长度过长,则回收一部分内存对象到central cache。

设计一个管理小对象的自由链表 FreeList

  1. 取到当前内存的下一个内存块的地址

void*& NextObj(void* obj)
{
    return *(void**)obj;
}
  1. 申请内存->pop

void* Pop()//申请节点
    {
        assert(_freeList);
        // 头删
        void* obj = _freeList;
        _freeList = NextObj(obj);

        return obj;
    }
  1. 释放内存到thread cache

void Push(void* obj)
    {
        assert(obj);
        // 头插
        //*(void**)obj = _freeList;
        NextObj(obj) = _freeList;
        _freeList = obj;
    }
  1. 判空

bool Empty()
    {
        return _freeList == nullptr;
    }

计算对象大小的对齐映射规则

由该图可以得知,若size<=128,则申请很多8byte,8byte对应的哈希桶挂接的链表有16个节点,一共128byte的空间,如果我要申请15字节,那就浪费1byte,申请两个这样的节点,alignNum为8byte,alignSize为16byte.....

[1,128] 8byte对齐 freelist[0,16) [128+1,1024] 16byte对齐 freelist[16,72) [1024+1,8*1024] 128byte对齐 freelist[72,128) [8*1024+1,64*1024] 1024byte对齐 freelist[128,184) [64*1024+1,256*1024] 8*1024byte对齐 freelist[184,208)

这样做的好处是,越到后面,浪费的比率越小,减少内碎片,因为我们不可能像定长内存池那样申请那么多节点,那样浪费太多,这样既可以减少内碎片,又只申请了208个节点

那该如何对应映射关系呢

1.已知需要的空间大小,可以得到应该申请多少字节单位的桶,然后需要求对齐以后实际申请的字节数

size_t _RoundUp(size_t size, size_t alignNum)
    {
        size_t alignSize;
        if (size % alignNum != 0)
        {
            alignSize = (size / alignNum + 1)*alignNum;
        }
        else
        {
            alignSize = size;
        }
        return alignSize;
    }
static inline size_t RoundUp(size_t size)
    {
        if (size <= 128)
        {
            return _RoundUp(size, 8);
        }
        else if (size <= 1024)
        {
            return _RoundUp(size, 16);
        }
        else if (size <= 8 * 1024)
        {
            return _RoundUp(size, 128);
        }
        else if (size <= 64 * 1024)
        {
            return _RoundUp(size, 1024);
        }
        else if (size <= 256 * 1024)
        {
            return _RoundUp(size, 8 * 1024);
        }
        else
        {
            assert(false);
            return -1;
        }
    }

由于计算机的位运算十分高效,因此我们可以学习一种优秀的算法

static inline size_t _RoundUp(size_t bytes, size_t alignNum)
    {
        return ((bytes + alignNum - 1) & ~(alignNum - 1));
    }

2. 计算映射的哪一个自由链表桶

已知需要申请空间大小,求他映射到哪一个自由链表桶

我们设计的内存池有五种桶,第一种桶节点大小为8byte,<=128bytes的挂过来,这样的桶链表有16条,第二种桶链表节点大小为16bytes,<=1024bytes的挂过来,这样的链表有56条,第三种桶链表节点大小为128bytes,<=8k的挂过来,这样的链表有56条,第四种桶链表节点大小为1k,<64k挂过来,这样的链表有56条,第五种链表节点大小为8k,<256k的挂过来,这样的链表有24条,例如字节数为1~8的挂在第一个桶,9~16挂第二个桶……

const size_t MAX_BYTES = 256 * 1024;//thread cache最大容量
size_t _Index(size_t bytes, size_t alignNum)
    {
    if (bytes % alignNum == 0)
    {
    return bytes / alignNum - 1;
    }
    else
    {
    return bytes / alignNum;
    }
    }
    // 计算映射的哪一个自由链表桶
    static inline size_t Index(size_t bytes)
    {
        assert(bytes <= MAX_BYTES);

        // 每个区间有多少个链
        static int group_array[4] = { 16, 56, 56, 56 };
        if (bytes <= 128) {
            return _Index(bytes, 8);
        }
        else if (bytes <= 1024) {
            return _Index(bytes - 128, 16) + group_array[0];
        }
        else if (bytes <= 8 * 1024) {
            return _Index(bytes - 1024, 128) + group_array[1] + group_array[0];
        }
        else if (bytes <= 64 * 1024) {
            return _Index(bytes - 8 * 1024, 1024) + group_array[2] + group_array[1] + group_array[0];
        }
        else if (bytes <= 256 * 1024) {
            return _Index(bytes - 64 * 1024, 8*1024) + group_array[3] + group_array[2] + group_array[1] + group_array[0];
        }
        else {
            assert(false);
        }

        return -1;
    }

高效的位运算

static inline size_t _Index(size_t bytes, size_t align_shift)
    {
        return ((bytes + (1 << align_shift) - 1) >> align_shift) - 1;
    }

    // 计算映射的哪一个自由链表桶
    static inline size_t Index(size_t bytes)
    {
        assert(bytes <= MAX_BYTES);

        // 每个区间有多少个链
        static int group_array[4] = { 16, 56, 56, 56 };
        if (bytes <= 128) {
            return _Index(bytes, 3);
        }
        else if (bytes <= 1024) {
            return _Index(bytes - 128, 4) + group_array[0];
        }
        else if (bytes <= 8 * 1024) {
            return _Index(bytes - 1024, 7) + group_array[1] + group_array[0];
        }
        else if (bytes <= 64 * 1024) {
            return _Index(bytes - 8 * 1024, 10) + group_array[2] + group_array[1] + group_array[0];
        }
        else if (bytes <= 256 * 1024) {
            return _Index(bytes - 64 * 1024, 13) + group_array[3] + group_array[2] + group_array[1] + group_array[0];
        }
        else {
            assert(false);
        }

        return -1;
    }

3.当前线程如何获取thread cache

前置知识:tls thread local storage线程的本地存储,对于所在的线程时全局的,但其他线程不可访问,数据独立性,避免用锁

class ThreadCache
{
public:
    // 申请和释放内存对象
    void* Allocate(size_t size);
    void Deallocate(void* ptr, size_t size);

    // 从中心缓存获取对象
    void* FetchFromCentralCache(size_t index, size_t size);
private:
    FreeList _freeLists[NFREELIST];
};

// TLS thread local storage
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;

void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{
    // ...
    return nullptr;
}

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);
    }
}

void ThreadCache::Deallocate(void* ptr, size_t size)
{
    assert(ptr);
    assert(size <= MAX_BYTES);

    // 找对映射的自由链表桶,对象插入进入
    size_t index = SizeClass::Index(size);
    _freeLists[index].Push(ptr);
}//暂时先给定size的大小,不然找不到桶

每个线程都有一份,并且获取自己的,pTLSThreadCache变量在他所在的线程内是全局可访问

4.创建线程,获取自己的thread cache,用tls指针来调用alloc分配对应内存

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

    cout << std::this_thread::get_id() << ":"<<pTLSThreadCache<<endl;

    return pTLSThreadCache->Allocate(size);
}

static void ConcurrentFree(void* ptr, size_t size)
{
    assert(pTLSThreadCache);

    pTLSThreadCache->Deallocate(ptr, size);
}

void Alloc1()
{
    for (size_t i = 0; i < 5; ++i)
    {
        void* ptr = ConcurrentAlloc(6);
    }
}

void Alloc2()
{
    for (size_t i = 0; i < 5; ++i)
    {
        void* ptr = ConcurrentAlloc(7);
    }
}


void TLSTest()
{//创建线程,执行alloc函数
    std::thread t1(Alloc1);
    t1.join();

    std::thread t2(Alloc2);
    t2.join();
}

int main()
{
    TLSTest();

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值