项目—高并发内存池

项目介绍

  • 内存池是一种动态内存分配与管理技术。通常情况下,程序员习惯直接使用malloc、free、new、delete等API申请内存与释放内存,这样导致的后果就是:当程序长时间运行时,由于所申请的内存块大小不一定,频繁使用时会造成大量的内存碎片从而降低程序与操作系统的性能
  • 内存池则是在真正的内存使用之前, 先申请分配一大块内存留作备用,当程序员申请内存时,从池中取出一块动态分配,当程序员释放内存时,将释放的内存再放入池中,再次申请时可以再次取出来用,并尽量与周边的空闲内存块合并。若内存池不够时,则自动扩大内存池,从操作系统中申请更大的内存池。
  • 高并发内存池相比于普通内存池,在多核多线程环境下,解决竞争问题,提高内存池的性能与安全性,同时也解决了内存碎片引发的引发的效率问题。

项目优点

  • 内存碎片问题
  • 性能问题
  • 多核多线程环境下的锁竞争问题

项目结构

高并发内存池主要由以下三个部分构成:
在这里插入图片描述

  1. ThreadCache:线程缓存是每个线程独有的。用于存放分配小于64k的内存,线程在这里申请内存不需要加锁,每个线程独享一个cache,提高了线程池的效率。
  2. CentralCache:中心缓存是所有线程所共享的。ThreadCache是按需从CentralCache中获取对象,CentralCache周期性的回收ThreadCache中的对象,避免一个线程占用太多资源,而其他线程内存吃紧。达到内存分配在多个线程中能够按需掉调度目的。CentrralCache是存在资源竞争的,所以在这里去对象需要加锁。
  3. PageCache:页缓存是在CentralCache上面的一层缓存,存储的内存是以页为单位存储分配的。当CentralCache中没有内存对象(span)时,就从 PageCache中分配出一定数量的page,并切割成定长大小的小块内存,分配给CentrealCache。PageCache会回收CenttrealCache满足条件的Span(使用计数为0)对象,并且合并相邻的页组成更大的页,缓解内存碎片问题。
  • 如何实现每个线程都拥有自己唯一的线程缓存?
    为了避免加锁带来的效率问题,在ThreadCache中使用thread local storage保存每个线程本地的ThreadCache的指针,这样ThreadCache在申请释放内存时是不需要加锁的。因为每一个线程都拥有了自己的唯一的一个全局变量。
ThreadCache
class ThreadCache{
public:
	void* Allocte(size_t size);//分配内存
	void Deallocte(void* ptr,size_t size);//释放内存
	void* FetchFromCentralCache(size_t index);//从中心缓存获取内存对象
	void ListTooLong(FreeList& freeList, size_t num, size_t size); //自由链表中对象过长释放回中心缓存
private:
	FreeList _freeLists[NFREE_LIST];//创建一个自由链表数组
};

FreeList是一个封装了一个普通指针、链表长度以及对于链表的操作方法的类。

自由链表的设计(对齐规则)
//控制有[1%,10%]左右的内碎片
// [1,128]              8byte对齐 freelist[0,16)
// [129,1024]           16byte对齐 freelist[16,72)
// [1025,8*1024]        128byte对齐 freelist[72,128)
// [8*1024+1,64*1024]   1024byte对齐 freelist[128,240)
//
class SizeClass{
public:
    //求出在该区间的第几个
	static size_t ListIndex(size_t size){
		assert(size <= MAX_SIZE);
		//每个区间上有多少个链
		static int group_array[4] = { 16, 56, 56, 112 };
		if (size <= 128){
			return _ListIndex(size, 3);
		}
		else if (size <= 1024){
			return _ListIndex(size - 128, 4) + group_array[0];
		}
		else if (size <= 8192){
			return _ListIndex(size - 1024, 7) + group_array[1] + group_array[0];
		}
		else if (size <= 65536){
			return _ListIndex(size - 8192, 10) + group_array[2] + group_array[1] + group_array[0];
		}
		return -1;
	}
    
	static size_t _ListIndex(size_t size, size_t align_shift){
		/*if (size % 8 == 0){
			return size / 8 - 1;
		} 
		else{
			return size / 8;
		}*/
		return ((size + (1 << align_shift) - 1) >> align_shift) - 1;
	}

	//向上取整
	static inline size_t RoundUp(size_t size)
	{
		assert(size <= MAX_SIZE);

		if (size <= 128){
			return _RoundUp(size, 8);
		}
		else if (size <= 1024){
			return _RoundUp(size, 16);
		}
		else if (size <= 8192){
			return _RoundUp(size, 128);
		}
		else if (size <= 65536){
			return _RoundUp(size, 1024);
		}

		return -1;
	}

	//向上对齐
	static size_t _RoundUp(size_t size, size_t align){
		/*if (size % 8 != 0){
			return (size / 8 + 1)*8;
			}
			else{
			return size;
			}*/

		//[9,16]+7=[16,23]->16 8 4 2 1  &后,低位变为0
		return (size + align - 1)&(~(align - 1));
	}

	static size_t NumMoveSize(size_t size)
	{
		if (size == 0)
			return 0;
		int num = MAX_SIZE / size;
		if (num < 2)
			num = 2;
		if (num > 512)
			num = 512;
		return num;
	}


	static size_t NumMovePage(size_t size){
		size_t num = NumMoveSize(size);
		size_t npage = num*size;
		npage >>= 12;
		if (npage == 0){
			return 1;
		}
		return npage;
	}

};
ThreadCache申请内存
  • 只能申请大小在64k范围以内的内存,如果大于64k,则调用VirtualAlloc直接向系统申请内存。
  • 当申请的内存size<=64k时在ThreadCache申请内存,先计算出size在自由链表中的位置i,如果自由链表中有内存对象,就直接从FreeList[i]中pop,然后返回对象,时间复杂度是O(1),没有锁竞争,效率极高;如果FreeList[i]中没有对象,则批量从CentralCache中获取一定数量的对象,剩余的n-1个对象插入到对应的自由链表中并返回一个对象。
ThreadCache释放内存
  • 当释放的内存小于64k时将内存释放回ThreadCache,先计算size在自由链表中的位置,然后将对象Push到FreeList[i]中。
  • 当自由链表的长度超过一次向中心缓存申请分配的内存块数目时,回收一部分内存对象到CentrealCache。
CentralCache
  • CentralCache本质上是由一个哈希映射的span对象双向链表构成。
  • 为了保证全局只有唯一的CentralCache,这个类可以被设计成单例模式。
  • 单例模式采用饿汉模式,避免高并发情况下资源的竞争。

在这里插入图片描述

span结构

一个span是由多个页组成的span对象。一页的大小是4k。

typedef unsigned int PAGE_ID;
struct Span{
	PAGE_ID _pageid=0;    //起始页号(有2^52个页号),一个span包含多个页
	PAGE_ID pagesize = 0; //页的数量

	FreeList _freeList;   //对象的自由自由链表
	int _usecount=0;      //内存块对象使用计数
	size_t objsize=0;     //自由链表对象大小
	
	Span* _next=nullptr;  //维护双向链表
	Span* _prev=nullptr;          
};
SpanList

SpanList设计为一个双向链表,插入删除效率很高。

class SpanList{
public:
	SpanList(){
		_head = new Span;
		_head->_next = _head;
		_head->_prev = _head;
	}
	
	Span* Begin(){
		return _head->_next;
	}

	Span* End(){
		return _head;
	}

	void PushFront(Span* newspan){
		Insert(_head->_next, newspan);
	}

	void PopFront(){
		Erase(_head->_next);
	}

	void PushBack(Span* newspan){
		Insert(_head, newspan);
	}

	void PopBack(){
		Erase(_head->_prev);
	}


	void Insert(Span* pos,Span* newspan){
		Span*  prev = pos->_prev;
		prev->_next = newspan;
		newspan->_next = pos;
		pos->_prev = newspan;
		newspan->_prev = prev;
	}

	void Erase(Span* pos){
		assert (pos != _head);
		Span *prev = pos->_prev;
		Span *next = pos->_next;
		prev->_next = next;
		next->_prev = prev;
	}

	bool Empty(){
		return Begin() == End();
	}

	void Lock(){
		_mtx.lock();
	}
	void Unlock(){
		_mtx.unlock();
	}

private:
	Span* _head=nullptr;
	std::mutex _mtx;  //给每一个SpanList桶加锁
};
CentralCache申请内存
  • 当ThreadCache中没有内存时,就会批量向CentralCache申请一定数量的内存对象,CentralCache也是一个哈希映射的SpanList,SpanList中挂着span,从span中取出对象给ThreadCache的这个过程是需要加锁的,否则当多个线程同时取对象时,就会导致线程安全问题。
  • 当CentralCache中没有非空的SpanList时,则将空的Span链在一起,然后向PageCache申请一个span对象,span对象中是一些以页为单位的内存,将这个span对象切成需要的内存大小并链接起来,最后挂到CentralCache中。
  • CentralCache中的每一个span都有一个usecount,分配一个对象给ThreadCache,就++usecount,当这个span的使用计数为0,说明这个span的使用计数为0,说明这个span所有的内存对象都是空闲的,就可以将它还给PageCache,合并成更大的页,减少内存碎片。
CentralCache释放内存
  • 当ThreadCache过长或者线程销毁,则会将内存释放回CentralCache中,没释放一个内存对象,检查该内存所在的span使用的计数是否为空,释放一个回来就–usecount。
  • 当usecount减到0时则表示所有对象都回到了span,则将span释放回PageCache,在PageCache中会对前后相邻的空闲也进行合并。
PageCache
  • PageCache是一个以页为单位的span自由链表。
  • 为了保证全局只有惟一的PageCache,这个类被设计成了单例模式。
  • 本单例模式采用饿汉模式。(饿汉模式,会在main函数之前就创建单例对象)
在这里插入代码片

在这里插入图片描述

PageCache申请内存
  • 当CentralCache向PageCache申请内存时,PageCaceh先检查对应位置有没有span,如果没有则向更页寻找一个span,如果找到则分裂这个页。
  • 如果找到128页都没有找到合适的span,则向系统使用mmp、brk(Linux)、或者VirtualAlloc(windows)等方式申请128page的span挂在自由链表中,再重复1中的过程。
PageCache释放内存
  • 如果CentralCache释放回一个span,则依次寻找span的前后page id的span,检查是否可以进行合并,如果可以合并则继续往前寻找。这样就可以将切小的内存合并收缩成大的span,减少内存碎片。但是合并的最大页数超过128页则不能合并。
  • 如果ThreadCache想直接申请大于64k的内存,直接去PageCache申请,当在PageCache申请时,如果申请的内存大于128页,则直接向系统申请这块内存,如果小于128页,则去SpanList去查找。
向系统申请内存
  • Linux平台下使用brk或sbrk向系统直接申请内存。
  • Windows平台下使用VirtualAlloc向系统申请堆内存。
项目的不足及扩展
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值