高并发内存池--tcmalloc

1. 项目介绍

1.1 这个项目是什么

今天要学习的项目的原型是Google开源项目tcmalloc ,全称Thread-Caching Malloc,即线程缓存的malloc,实现了高效的多线程内存管理,用于替代系统的内存分配相关函数(malloc、free)。

此项目可以说是顶尖C++高手写出来了的,我们学习他不是为了造更好的轮子,而是学习其思想,学习如何去思考问题,我将把tcmalloc最核心的框架简化后拿出来,模拟实现一个自己的高并发内存池。

1.2 这个项目的意义

相信大家都有一个疑惑,既然系统已经提供了malloc、free,为什么Google的大佬要额外实现一个tcmalloctcmallocmalloc有什么区别

我们要知道,malloc是C标准库函数,应该要保证在各种情况下都能够实现内存管理,简单来说就是通用,而通用就意味着不能保证各种情况下都高效,二者不可兼得,实际是malloc中为了保证线程安全,加锁解锁消耗了非常多的性能。malloc在单线程下性能比较高,但是在多线程下性能就比较低了,这个时候tcmalloc孕育而生。

1.3 项目准备

这个项目会用到C/C++、数据结构(链表、哈希桶)、操作系统内存管理、单例模式、多线程、互斥锁等等方面的知识。

2. 什么是内存池

2.1 池化技术

所谓“池化技术”,就是程序先向系统申请过量的资源,然后自己管理,以备不时之需。之所以要申请过量的资源,是因为每次申请该资源都有较大的开销,不如提前申请好了,这样使用时就会变得非常快捷,大大提高程序运行效率。

在计算机中,有很多使用“池”这种技术的地方,除了内存池,还有连接池、线程池、对象池等。以服务器上的线程池为例,它的主要思想是:先启动若干数量的线程,让它们处于睡眠状态,当接收到客户端的请求时,唤醒池中某个睡眠的线程,让它来处理客户端的请求,当处理完这个请求,线程又进入睡眠状态。

2.2 内存池

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

2.3 内存池主要解决的问题

  1. 内存池最主要解决的是效率问题
  2. 其次还有内存碎片问题

内存碎片分为:内碎片和外碎片

  1. 内碎片:你申请了5B的内存,但系统实际给你了8B内存,那么这多出来的3B就是内碎片,即实际分配的内存比所需内存多的那部分就是内碎片

  2. 外碎片:
    在这里插入图片描述
    即因为内存中不连续的小块内存,当需要大块内存时,小块内存因为不连续,导致无法分配出去

2.4 malloc

C/C++中我们要动态申请内存都是通过malloc去申请内存,但是我们要知道,实际我们不是直接去堆获取内存的,
而malloc就是一个内存池malloc()相当于向操作系统“批发”了一块较大的内存空间,然后“零售”给程序用。当全部“售完”或程序有大量的内存需求时,再根据实际需求向操作系统“进货”。malloc的实现方式有很多种,一般不同编译器平台用的都是不同的。比如windows的vs系列用的微软自己写的一套,linux gcc用的glibc中的ptmalloc

下面有几篇关于这块的文章,关于ptmalloc,有兴趣大家可以去看看他的实现细节。

一文了解,Linux内存管理,malloc、free 实现原理
malloc()背后的实现原理——内存池
malloc的底层实现(ptmalloc)

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

现代很多的开发环境都是多核多线程,在申请内存的场景下,必然存在激烈的锁竞争问题。malloc本身其实已经很优秀,那么我们项目的原型tcmalloc就是在多线程高并发的场景下更胜一筹,所以这次我们实现的内存池需要考虑以下几方面的问题

  1. 性能问题。
  2. 多线程环境下,锁竞争问题。
  3. 内存碎片问题。

concurrent memory pool主要由以下3个部分构成:

  1. thread cache:线程缓存是每个线程独有的,用于小于256KB的内存的分配,线程从这里申请内存不需要加锁,每个线程独享一个cache,这也就是这个并发线程池高效的地方。
  2. central cache:中心缓存是所有线程所共享,thread cache是按需从central cache中获取的对象。central cache合适的时机回收thread cache中的对象,避免一个线程占用了太多的内存,而其他线程的内存吃紧,达到内存分配在多个线程中更均衡的按需调度的目的。central cache是存在竞争的,所以从这里取内存对象是需要加锁,首先这里用的是桶锁,其次只有thread cache的没有内存对象时才会找central cache,所以这里竞争不会很激烈。
  3. page cache:页缓存是在central cache缓存上面的一层缓存,存储的内存是以页为单位存储及分配的,central cache没有内存对象时,从page cache分配出一定数量的page,并切割成定长大小的小块内存,分配给central cache。当一个span的几个跨度页的对象都回收以后,page cache会回收central cache满足条件的span对象,并且合并相邻的页,组成更大的页,缓解内存碎片的问题。

在这里插入图片描述

4. 申请内存

为方便学习,我们先只考虑高并发内存池的申请内存逻辑

4.1 thread cache

thread cache是哈希桶结构,每个桶是一个按桶位置映射大小的内存块对象的自由链表。每个线程都会有一个thread cache对象,这样每个线程在这里获取对象和释放对象时是无锁的
在这里插入图片描述
核心:以申请的内存块大小作为索引,来寻找对应的桶

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

在设计其时,需要考虑一些细节:

  1. 申请的最小内存应该为多大? – 8B,因为对于每个小块内存,都需要在其内部存储一个指向下一个小块内存的指针,在32位下指针大小为4B,在64位下指针大小为8B,为了保证兼容两种模式,所以最小内存应为8B
  2. 每个桶之间应该间隔多大内存? 这个可以自行设定,需要考虑的是如果间隔为8B,那么需要256KB/8B = 32K个桶,非常庞大的数字,所以设置一个合适的间隔也比较重要
  3. 当我们申请的内存并不是刚好等于桶中小块内存大小时,我们需要向上取整,例如我们申请5B的内存,实际应该匹配到8B的小块内存,多出来的3B是内碎片,为了使桶的个数不要太多,无法避免内碎片

//小块内存头4/8个字节存储指向下一个小块内存
//用于找到下一个小块内存
static void*& NextObj(void* obj)
{
	return *(void**)obj;
}

class FreeList {
public:
	void PushFront(void* obj)
	{
		NextObj(obj) = _freeList;
		_freeList = obj;
		_size++;
	}
	void PushRangeFront(void* begin, void* end,size_t n)
	{
		NextObj(end) = _freeList;
		_freeList = begin;
		_size += n;
	}
	void* PopFront()
	{
		void* obj = _freeList;
		_freeList = NextObj(_freeList);
		_size--;
		return obj;
	}
	void PopRangeFront(void*& begin,void*& end,size_t n)
	{
		assert(n <= _size);
		begin = _freeList;
		void* cur = _freeList;
		for (size_t i = 1; i < n; i++)
		{
			cur = NextObj(cur);
		}
		end = cur;
		NextObj(end) = nullptr;
		_freeList = NextObj(cur);
		_size -= n;
	}
	bool Empty()
	{
		return _size == 0;
	}
	size_t& MaxSize()
	{
		return _maxSize;
	}
	size_t Size()
	{
		return _size;
	}
private:
	void* _freeList = nullptr;
	size_t _maxSize = 1; //向centralcache申请内存次数,用于慢启动获取小块内存数
	size_t _size = 0; //链表节点个数
};

class ThreadCache {
public:
	//向thread cache申请内存
	void* Allocate(size_t size);
	
	//当thread cache没有对应小块内存时,会从central cache中获取小块内存
	void* FetchFromCentralCache(size_t index, size_t size);

-------------------------------------------------------------------------
//释放逻辑
	void Deallocate(void* ptr,size_t size);

	void ListTooLong(FreeList& list, size_t size);
private:
	FreeList _freeLists[FREELISTS_NUM];
		
};

//thread local storage技术(TLS) 虽然声明是全局静态变量,但是能够保证每个进程只有一份 
static __declspec(thread) ThreadCache* pTLSThreadCache = nullptr;

我们需要确定桶的个数FREELISTS_NUM,桶内存块间隔
我采用的是分段确定:threadCache中每个桶所含的内存块大小不一

  • [1B,128B] 中有16个桶,相邻桶之间内存大小相差8B
  • [129B,1KB] 有56个桶,相邻桶之间内存大小相差16B
  • [1KB+1B,8KB] 有56个桶,相邻桶之间内存大小相差128B
  • [8KB+1B,64KB] 有56个桶,相邻桶之间内存大小相差1KB
  • [64KB+1B,256KB] 有24个桶,相邻桶之间内存大小相差8KB
  • 总计208个桶

这样的设计能保证每个小块内存的内碎片最大在百分之10左右

我们需要一个用于对齐的类:SizeClass

class SizeClass {
public:
	//对所需内存字节数向上取整
	static size_t _RoundUp(size_t size,size_t align)
	{
		return  (size + align - 1) & ~(align - 1);
	}
	static 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 <= MAX_BYTES) return _RoundUp(size, 8*1024);
		else {
			//这里是超过MAX_BYTES的情况
			//超过MAX_BYTES则直接按页对齐
			return _RoundUp(size, 1 << PAGE_SHIFT);
		}
	}


	static size_t _Index(size_t size,size_t align_shift)
	{
		return ((size + (1 << align_shift) - 1) >> align_shift) - 1;
	}
	//对所需字节数向上取整,并得到其桶下标
	static size_t Index(size_t size)
	{
		assert(size <= MAX_BYTES);
		/*
		* threadCache中每个桶所含的内存块大小不一
		* [1,128] 中有16个桶,相邻桶之间内存大小相差8B
		* [129,1024] 有56个桶,相邻桶之间内存大小相差16B
		* [1025,8*1024] 有56个桶,相邻桶之间内存大小相差128B
		* [8*1024+1,64*1024] 有56个桶,相邻桶之间内存大小相差1024B
		* [64*1024+1,256*1024] 有24个桶,相邻桶之间内存大小相差8*1024B
		*/
		static const int group_array[] = { 16,56,56,56,24 };
		if (size <= 128) return _Index(size, 3);
		else if (size <= 1024) {
			return _Index(size - 128, 4) + group_array[0];
		}
		else if (size <= 8 * 1024) {
			return _Index(size - 1024, 7) + group_array[1] + group_array[0];
		}
		else if (size <= 64 * 1024) {
			return _Index(size - 8 * 1024, 10) + group_array[2] + group_array[1] + group_array[0];
		}
		else if (size <= 256 * 1024) {
			return _Index(size - 64 * 1024, 13) + group_array[3] + group_array[2] + group_array[1] + group_array[0];
		}
		else assert(false);
		return -1;
	}

	//threadcache一次向centralcache要的obj数量的上限
	static size_t NumMoveSize(size_t size)
	{
		assert(size > 0);
		//慢启动策略
		//小对象一次批量申请的上限高
		//大对象一次批量申请的上限低
		size_t num = MAX_BYTES / size;
		if (num < 2) num = 2;
		else if (num > 512) num = 512;
		return num;
	}
	//central cache一次向page cache索要的span的页数
	static size_t NumMovePage(size_t size)
	{
		size_t num = NumMoveSize(size);
		size_t pages = num * size;
		pages >>= PAGE_SHIFT;
		if (pages < 1) pages = 1;
		return pages;
	}
}

向thread cache申请小块内存具体实现

//内存池最大申请可申请字节数
static const size_t MAX_BYTES = 256 * 1024;
//ThreadCache和CentralCache中桶的个数
static const size_t FREELISTS_NUM = 208;

void* ThreadCache::Allocate(size_t size)
{
	//申请的内存必须合法
	assert(size > 0 && size <= MAX_BYTES);
	size_t alignSize = SizeClass::RoundUp(size);
	size_t index = SizeClass::Index(size);
	//如果桶上有小块内存则返回给用户
	if (!_freeLists[index].Empty())
	{
		return _freeLists[index].PopFront();
	}
	//链表中没有空闲内存需要向CentralCache申请
	else
	{
		return FetchFromCentralCache(index, alignSize);
	}
}

//从CentralCache中获取多个小块内存
void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{
	/* 慢启动
	* 每个桶有自己的maxSize,表示申请过多少次,每次成功申请都会增加其值
	* 要申请的空闲内存块数 = min(maxSize,MAX_BYTES/size)
	* MAX_BYTES/size:小对象申请的上限大,大对象申请的上限小
	*/
	size_t batch = min(_freeLists[index].MaxSize(), SizeClass::NumMoveSize(size));
	if (batch == _freeLists[index].MaxSize()) _freeLists[index].MaxSize()++;

	void* start = nullptr;
	void* end = nullptr;
	//FetchRangeObj是CentralCache的方法
	size_t actual_batch = CentralCache::GetInstance()->FetchRangeObj(start,end, batch,size);
	//如果actual_num为0,则表示内存获取失败,
	// 1.要么申请内存没申请到,则在申请处会抛异常的,不会走到这
	// 2.要么程序逻辑错误,应该断言检查
	assert(actual_batch > 0);

	if (actual_batch > 1)
	{
		_freeLists[index].PushRangeFront(NextObj(start), end, actual_batch - 1);
	}
	else
	{
		assert(start == end);
	}
	return start;
}

4.2 central cache

central cache也是一个哈希桶结构,他的哈希桶的映射关系跟thread cache是一样的。不同的是他的每个哈希桶位置挂是SpanList链表结构,不过每个映射桶下面的span中的大内存块被按映射关系切成了一个个小内存块对象挂在span的自由链表中。
在这里插入图片描述
这里重点要理解Span类,
其有如下字段:

//Span一个跨度的大块内存
// 管理以页为单位的大块内存
// 管理多个连续页大块内存跨度结构
struct Span {
	//页号
	PAGE_ID _pageId = 0;
	//页数
	size_t _n = 0;
	//指向下一个Span
	Span* _next = nullptr;
	//指向前一个Span
	Span* _prev = nullptr;
	//指向小对象单链表的第一个节点,串起一堆小对象
	void* _freeList = nullptr;
----------------------------------------------------------
//这三个字段,在释放逻辑会详细讲解
	//所含每个小对象的大小
	size_t _objSize = 0;
	//已经被分配给thread cache的小对象个数
	size_t _useCount = 0;
	//这个Span是否从page cache上取下
	bool _isUse = true;
};
  • pageId:即页号,现在不需要太琢磨与它,待会在page cache处我会详细讲解,现在只需认为,它可以转变为向系统申请的内存的首地址
  • n:即页数,表示当前Span管理着多少页内存
  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中useCount记录分配了多少个对象出去,分配一个对象给threadcache,就++useCount

CentralCache全局只有一个,所以需要用到单例模式,这里采用饿汉模式

class CentralCache {
public:
	//在对应的桶中获取一个含有小对象的Span
	Span* GetOneSpan(size_t index,size_t size);
	
	//获取一批小对象
	size_t FetchRangeObj(void*& begin, void*& end, size_t batch, size_t size);

	//释放逻辑
	void ReleaseListToSpans(void* begin,size_t size);

	static CentralCache* GetInstance()
	{
		return &_sInst;
	}
private:
	SpanList _spanLists[FREELISTS_NUM];

	static CentralCache _sInst;
private:
	CentralCache()
	{}
	CentralCache(const CentralCache&) = delete;
	CentralCache& operator=(const CentralCache&) = delete;
};
CentralCache CentralCache::_sInst;

//获取一些小块内存
size_t CentralCache::FetchRangeObj(void*& begin, void*& end, size_t batch, size_t size)
{
	size_t index = SizeClass::Index(size);
	
	_spanLists[index].Mutex()->lock();
	Span* span = GetOneSpan(index,size);
	assert(span);
	assert(span->_freeList != nullptr);
	
	//从span的小块内存中,选取一部分
	void* cur = span->_freeList;
	batch--;
	size_t actualNum = 1;
	begin = cur;
	while (batch > 0 && NextObj(cur) != nullptr)
	{
		cur = NextObj(cur);
		actualNum++;
		batch--;
	}
	span->_freeList = NextObj(cur);
	NextObj(cur) = nullptr;
	end = cur;
	span->_useCount += actualNum;

	_spanLists[index].Mutex()->unlock();
	return actualNum;
}

//从spanList上获取一个含有空闲小块内存的span
Span* CentralCache::GetOneSpan(size_t index,size_t size)
{
	//首先找对应的SpanList,查看是否有Span含有小对象内存
	Span* it = _spanLists[index].Begin();
	while (it != _spanLists[index].End())
	{
		if (it->_freeList != nullptr)
		{
			return it;
		}
		it = it->_next;
	}
	_spanLists[index].Mutex()->unlock();
	//走到这里说明没有空闲Span了,需要向PageCache要
	
	PageCache::GetInstance()->Mutex()->lock();
	Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));
	PageCache::GetInstance()->Mutex()->unlock();

	//从PageCache获取到Span后,将Span所管理的大块内存,切成许多小块内存,挂在freeList上

	//获取从PageCache得到的大块内存的首尾地址
	char* start = (char*)(span->_pageId << PAGE_SHIFT);
	size_t bytes = span->_n << PAGE_SHIFT;
	char* end = start + bytes;

	//将大块内存切分为一块一块小内存,并链接到freeList上
	span->_freeList = start;
	start += size;
	void* tail = span->_freeList;
	while (start < end)
	{
		NextObj(tail) = start;
		tail = NextObj(tail);
		start += size;
	}
	NextObj(tail) = nullptr;
	span->_objSize = size;
	_spanLists[index].Mutex()->lock();
	_spanLists[index].PushFront(span);
	return span;
}

4.3 page cache

page cache也是一个哈希桶,桶的下标是根据Span所含页数确定的,1到128页就有128个桶,每个桶存储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中的过程。
  3. 需要注意的是central cache和page cache 的核心结构都是spanlist的哈希桶,但是他们是有本质区别的,central cache中哈希桶,是按跟thread cache一样的大小对齐关系映射的,他的spanlist中挂的span中的内存都被按映射关系切好链接成小块内存的自由链表。而page cache 中的spanlist则是按下标桶号映射的,也就是说第i号桶中挂的span都是i页内存。

PageCache是系统和CentralCache之间的桥梁,负责向系统申请大块内存,然后将内存构建成Span,以便CentralCache使用
PageCache是按页的倍数向系统申请内存的,然后将得到的内存首地址除上一页的大小,就能得到页号,即pageId,而再加一个页数n,即可管理整个大块内存

PageCache结构:PageCache也采用单例模式

//PageCache的桶数
static const size_t KPAGE = 129;
//一个Page的大小:2^13 -- 可以改进一下,先获取系统一页的大小,然后再确定
static const size_t PAGE_SHIFT = 13;

class PageCache {
public:
	static PageCache* GetInstance()
	{
		return &_sInst;
	}
	//获取一个新的Span
	Span* NewSpan(size_t kpages);

	//根据地址索引到Span
	Span* MapObjToSpan(void* obj);

	//释放Span
	void ReleaseSpan(Span* span);

	std::mutex* Mutex()
	{
		return &_mtx;
	}
private:
	SpanList _pageLists[KPAGE];

	//pageId索引得到Span
	std::unordered_map<PAGE_ID, Span*> _idSpanMap;
	std::mutex _mtx;

private:
	PageCache()
	{}
	PageCache(const PageCache&) = delete;
	PageCache& operator=(const PageCache&) = delete;
	static PageCache _sInst;
};
Span* PageCache::NewSpan(size_t kpages)
{
	assert(kpages >= 1);

	//检查第一个桶有没有Span
	if (!_pageLists[kpages].Empty())
	{
		Span* span = _pageLists[kpages].PopFront();
		//将分出给CentralCache的span每个页与其指针映射起来
		for (size_t i = 0; i < span->_n; i++)
		{
			_idSpanMap[span->_pageId + i] = span;
		}
		span->_isUse = true;
		return span;
	}
	
	for (size_t i = kpages + 1; i < KPAGE; i++)
	{
		//如果比kpages大的桶中有span则直接分割span
		if (!_pageLists[i].Empty())
		{
			Span* span = _pageLists[i].PopFront();
			Span* newSpan = new Span;
			newSpan->_pageId = span->_pageId;
			newSpan->_n = kpages;

			span->_n -= kpages;
			span->_pageId += kpages;
			_pageLists[span->_n].PushFront(span);

			//如果是在PageCache中保存的页,只需要将其首尾页进行映射即可
			_idSpanMap[span->_pageId]=span;
			_idSpanMap[span->_pageId + span->_n - 1] = span;

			//将分出给CentralCache的span每个页与其指针映射起来
			for (size_t i = 0; i < newSpan->_n; i++)
			{
				_idSpanMap[newSpan->_pageId + i] =newSpan;
			}

			return newSpan;
		}
	}
	//如果比kpages大的桶中没有span,则向系统申请

	//像系统申请的内存根据其地址,直接映射为页号
	//只要保证PAGE和系统页面是一样大小就行

	void* ptr = SystemAlloc(KPAGE-1);
	Span* span = new Span;
	span->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
	span->_n = KPAGE-1;
	_pageLists[span->_n].PushFront(span);
	return NewSpan(kpages);
}

5. 释放内存

5.1 thread cache

  1. 当释放内存小于256k时将内存释放回thread cache,计算size映射自由链表桶位置i,将对象Push到_freeLists[i]。
  2. 当链表的长度过长,则回收一部分内存对象到central cache。
//释放对象内存给ThreadCache
void ThreadCache::Deallocate(void* ptr, size_t size)
{
	assert(size > 0 && size <= MAX_BYTES);
	size_t index = SizeClass::Index(size);
	_freeLists[index].PushFront(ptr);
	
	//如果现在ThreadCache中自由链表空闲内存过多就将其打包释放给CentralCache
	//此实现细节只考虑了自由链表过长的情况,还可以加上自由链表所含闲置内存达到一个阈值时触发
	if (_freeLists[index].MaxSize() <= _freeLists[index].Size())
	{
		ListTooLong(_freeLists[index], size);
	}
}

void ThreadCache::ListTooLong(FreeList& list, size_t size)
{
	void* begin;
	void* end;
	list.PopRangeFront(begin, end, list.MaxSize());
	CentralCache::GetInstance()->ReleaseListToSpans(begin, size);
}

5.2 central cache

当thread_cache过长或者线程销毁,则会将内存释放回central cache中的,释放回来时–useCount。当useCount减到0时则表示所有对象都回到了span,则将span释放回page cache,page cache中会对前后相邻的空闲页进行合并。

//将ThreadCache释放的小块内存重新挂到Span上,当此span小块内存都被归还时,再释放到PageCache中
void CentralCache::ReleaseListToSpans(void* begin,size_t size)
{
	size_t index = SizeClass::Index(size);
	_spanLists[index].Mutex()->lock();
	//将每块小内存通过映射找到其对应的span,并挂上去
	while (begin != nullptr)
	{
		void* next = NextObj(begin);
		Span* span = PageCache::GetInstance()->MapObjToSpan(begin);
		//头插入span的freeList上
		NextObj(begin) = span->_freeList;
		span->_freeList = begin;
		span->_useCount--;
		begin = next;
		//如果span的所有小块内存均为被使用则将其回收到PageCache中
		if (span->_useCount == 0)
		{
			_spanLists[index].Erase(span);
			_spanLists[index].Mutex()->unlock();
			PageCache::GetInstance()->Mutex()->lock();
			PageCache::GetInstance()->ReleaseSpan(span);
			PageCache::GetInstance()->Mutex()->unlock();
			_spanLists[index].Mutex()->lock();

		}
	}
	_spanLists[index].Mutex()->unlock();
}

由于需要通过小块内存首地址索引到对应的Span,PageCache应该提供MapObjToSpan(void* obj)以供索引

//将对象地址转换为页号,寻找对应的span
Span* PageCache::MapObjToSpan(void* obj)
{
	PAGE_ID id = (PAGE_ID)obj >> PAGE_SHIFT;
	std::unique_lock<std::mutex> uniqueMtx(*PageCache::GetInstance()->Mutex());
	auto ret = _idSpanMap.find(id);
	if (ret != _idSpanMap.end())
	{
		return ret->second;
	}
	if (span != nullptr)
		return span;
	//一定是能找到的,如果找不到则说明发生了错误
	else
	{
		assert(false);
		return nullptr;
	}
}

5.3 page cache

如果central cache释放回一个span,则依次寻找span的前后page id的没有在使用的空闲span,看是否可以合并,如果合并继续向前寻找。这样就可以将切小的内存合并收缩成大的span,减少内存碎片。

//将CentralCache传来的空闲span与页号相邻的span合并
void PageCache::ReleaseSpan(Span* span)
{
	//先将小于此span第一页的相邻页合并
	while (1)
	{
		PAGE_ID id = span->_pageId;
		auto ret = _idSpanMap.find(id - 1);
		//如果前一个span不存在,则退出
		if (ret == _idSpanMap.end()) break;

		Span* prevSpan = ret->second;

		//如果前一个span还在被使用,则退出
		//此处不能使用useCount,因为当span刚被申请出来,此时为0,但是不能将其合并
		if (prevSpan->_isUse) break;

		//如果与前一个span合并后超过了PageCache能挂的最大Span,则退出
		if (prevSpan->_n + span->_n > KPAGE - 1) break;

		_pageLists[prevSpan->_n].Erase(prevSpan);
		span->_pageId = prevSpan->_pageId;
		span->_n += prevSpan->_n;
		delete prevSpan;
	}
	//将在此span页之后的相邻span合并
	while (1)
	{
		PAGE_ID id = span->_pageId;
		auto ret = _idSpanMap.find(id + span->_n);

		//如果后一个span不存在,则退出
		if (ret == _idSpanMap.end()) break;

		Span* nextSpan = ret->second;
		//如果前一个span还在被使用,则退出
		//此处不能使用useCount,因为当span刚被申请出来,此时为0,但是不能将其合并
		if (nextSpan->_isUse) break;

		//如果与后一个span合并后超过了PageCache能挂的最大Span,则退出
		if (nextSpan->_n + span->_n > KPAGE - 1) break;

		_pageLists[nextSpan->_n].Erase(nextSpan);
		span->_n += nextSpan->_n;
		delete nextSpan;
	}

	//合并后要修改pageId和span的索引,不然会找到之前被释放的span

	//_idSpanMap[span->_pageId] = span;
	//_idSpanMap[span->_pageId + span->_n - 1] = span;

	_idSpanMap[span->_pageId] = span;
	_idSpanMap[span->_pageId + span->_n - 1] = span;

	span->_isUse = false;
	_pageLists[span->_n].PushFront(span);
}

6. 对象池取代new、delete

template<class T>
class ObjectPool {
	static const size_t MAX_BYTES = 128*1024;
	inline static void* SystemAlloc(size_t size)
	{
		assert(size > 0);
#ifdef _WIN32
		void* ptr = VirtualAlloc(0, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
//linux
#endif
		if (ptr == nullptr) throw std::bad_alloc();
		return ptr;
	}
	inline static void*& NextObj(void* obj)
	{
		return *(void**)obj;
	}
public:
	T* New()
	{
		T* obj = nullptr;
		_mtx.lock();
		if (_freeList != nullptr)
		{
			obj = (T*)_freeList;
			_freeList = NextObj(_freeList);
		}
		else		
		{
			if (_remainSize < sizeof(T))
			{
				size_t allocSize = max(sizeof(T), MAX_BYTES);
				_mem = (char*)SystemAlloc(allocSize);
				_remainSize = allocSize;
			}
			size_t size = max(sizeof(T), sizeof(void*));
			_remainSize -= size;
			obj = (T*)_mem;
			_mem += size;
		}
		_mtx.unlock();
		new(obj)T;
		return obj;
	}
	void Delete(T* obj)
	{
		obj->~T();
		_mtx.lock();
		NextObj(obj) = _freeList;
		_freeList = obj;
		_mtx.unlock();
	}

private:
	char* _mem = nullptr;
	size_t _remainSize = 0;
	void* _freeList = nullptr;
	std::mutex _mtx;
};

利用了池化技术,先申请一大块内存自己管理

于是可以将项目中的new与delete替换为对象池的New和Delete

7. 用户接口

要为用户封装一个接口以调用申请和释放函数

static void* ConcurrentAllocate(size_t size)
{
	if (pTLSThreadCache == nullptr)
	{
		static ObjectPool<ThreadCache> threadPool;
		pTLSThreadCache = threadPool.New();
	}
	assert(pTLSThreadCache);
	if (size > MAX_BYTES)
	{
		size_t alignSize = SizeClass::RoundUp(size);
		size_t kpage = alignSize >> PAGE_SHIFT;
		PageCache::GetInstance()->Mutex()->lock();
		Span* span = PageCache::GetInstance()->NewSpan(kpage);
		PageCache::GetInstance()->Mutex()->unlock();
		span->_objSize = alignSize;
		return (void*)(span->_pageId << PAGE_SHIFT);
	}
	else
	{
		return pTLSThreadCache->Allocate(size);
	}
}

static void ConcurrentFree(void* ptr)
{
	assert(pTLSThreadCache);
	PAGE_ID id = (PAGE_ID)ptr >> PAGE_SHIFT;
	Span* span = PageCache::GetInstance()->MapObjToSpan(ptr);
	size_t size = span->_objSize;
	if (size > MAX_BYTES)
	{
		PageCache::GetInstance()->Mutex()->lock();
		PageCache::GetInstance()->ReleaseSpan(span);
		PageCache::GetInstance()->Mutex()->unlock();
	}
	else  pTLSThreadCache->Deallocate(ptr,size);
}

8. 优化项目性能

通过测试会发现,MapObjToSpan(void* obj)占用了大量性能,这是因为unordered_map在增加元素时,可能导致结构变化,所以在使用时需要加锁,但是其调用较为频繁,大量的申请和释放锁,肯定会让性能大打折扣

我们可以利用radix基数树替换unordered_map,以达到访问设置索引时无需加锁。
radix是在trie的基础上,优化了空间而产生的

可以通过具体代码来理解此结构

// Single-level array
template <int BITS>
class TCMalloc_PageMap1 {
private:
	static const int LENGTH = 1 << BITS;
	void** array_;

public:
	typedef uintptr_t Number;

	//explicit TCMalloc_PageMap1(void* (*allocator)(size_t)) {
	explicit TCMalloc_PageMap1() {
		//array_ = reinterpret_cast<void**>((*allocator)(sizeof(void*) << BITS));
		size_t size = sizeof(void*) << BITS;
		size_t alignSize = SizeClass::_RoundUp(size, 1 << PAGE_SHIFT);
		array_ = (void**)SystemAlloc(alignSize >> PAGE_SHIFT);
		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 {
		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;
	}
};

// Two-level radix tree
template <int BITS>
class TCMalloc_PageMap2 {
private:
	// Put 32 entries in the root and (2^BITS)/32 entries in each leaf.
	static const int ROOT_BITS = 5;
	static const int ROOT_LENGTH = 1 << ROOT_BITS;

	static const int LEAF_BITS = BITS - ROOT_BITS;
	static const int LEAF_LENGTH = 1 << LEAF_BITS;

	// Leaf node
	struct Leaf {
		void* values[LEAF_LENGTH];
	};

	Leaf* root_[ROOT_LENGTH];             // Pointers to 32 child nodes
	void* (*allocator_)(size_t);          // Memory allocator

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) {
				//Leaf* leaf = reinterpret_cast<Leaf*>((*allocator_)(sizeof(Leaf)));
				//if (leaf == NULL) return false;
				static ObjectPool<Leaf>	leafPool;
				Leaf* leaf = (Leaf*)leafPool.New();

				memset(leaf, 0, sizeof(*leaf));
				root_[i1] = leaf;
			}

			// Advance key past whatever is covered by this leaf node
			key = ((key >> LEAF_BITS) + 1) << LEAF_BITS;
		}
		return true;
	}

	void PreallocateMoreMemory() {
		// Allocate enough to keep track of all possible pages
		Ensure(0, 1 << BITS);
	}
};

// Three-level radix tree
template <int BITS>
class TCMalloc_PageMap3 {
private:
	// How many bits should we consume at each interior level
	static const int INTERIOR_BITS = (BITS + 2) / 3; // Round-up
	static const int INTERIOR_LENGTH = 1 << INTERIOR_BITS;

	// How many bits should we consume at leaf level
	static const int LEAF_BITS = BITS - 2 * INTERIOR_BITS;
	static const int LEAF_LENGTH = 1 << LEAF_BITS;

	// Interior node
	struct Node {
		Node* ptrs[INTERIOR_LENGTH];
	};

	// Leaf node
	struct Leaf {
		void* values[LEAF_LENGTH];
	};

	Node* root_;                          // Root of radix tree
	void* (*allocator_)(size_t);          // Memory allocator

	Node* NewNode() {
		Node* result = reinterpret_cast<Node*>((*allocator_)(sizeof(Node)));
		if (result != NULL) {
			memset(result, 0, sizeof(*result));
		}
		return result;
	}

public:
	typedef uintptr_t Number;

	explicit TCMalloc_PageMap3(void* (*allocator)(size_t)) {
		allocator_ = allocator;
		root_ = NewNode();
	}

	void* get(Number k) const {
		const Number i1 = k >> (LEAF_BITS + INTERIOR_BITS);
		const Number i2 = (k >> LEAF_BITS) & (INTERIOR_LENGTH - 1);
		const Number i3 = k & (LEAF_LENGTH - 1);
		if ((k >> BITS) > 0 ||
			root_->ptrs[i1] == NULL || root_->ptrs[i1]->ptrs[i2] == NULL) {
			return NULL;
		}
		return reinterpret_cast<Leaf*>(root_->ptrs[i1]->ptrs[i2])->values[i3];
	}

	void set(Number k, void* v) {
		assert(k >> BITS == 0);
		const Number i1 = k >> (LEAF_BITS + INTERIOR_BITS);
		const Number i2 = (k >> LEAF_BITS) & (INTERIOR_LENGTH - 1);
		const Number i3 = k & (LEAF_LENGTH - 1);
		reinterpret_cast<Leaf*>(root_->ptrs[i1]->ptrs[i2])->values[i3] = v;
	}

	bool Ensure(Number start, size_t n) {
		for (Number key = start; key <= start + n - 1;) {
			const Number i1 = key >> (LEAF_BITS + INTERIOR_BITS);
			const Number i2 = (key >> LEAF_BITS) & (INTERIOR_LENGTH - 1);

			// Check for overflow
			if (i1 >= INTERIOR_LENGTH || i2 >= INTERIOR_LENGTH)
				return false;

			// Make 2nd level node if necessary
			if (root_->ptrs[i1] == NULL) {
				Node* n = NewNode();
				if (n == NULL) return false;
				root_->ptrs[i1] = n;
			}

			// Make leaf node if necessary
			if (root_->ptrs[i1]->ptrs[i2] == NULL) {
				Leaf* leaf = reinterpret_cast<Leaf*>((*allocator_)(sizeof(Leaf)));
				if (leaf == NULL) return false;
				memset(leaf, 0, sizeof(*leaf));
				root_->ptrs[i1]->ptrs[i2] = reinterpret_cast<Node*>(leaf);
			}

			// Advance key past whatever is covered by this leaf node
			key = ((key >> LEAF_BITS) + 1) << LEAF_BITS;
		}
		return true;
	}

	void PreallocateMoreMemory() {
	}
};

大佬们在实现了三种结构:

  1. 第一种为直接哈希
  2. 第二种为两层结构,例:假设32位下,一页大小为8K,那么页号只有32-13 = 19位,通过页号找到Span的方式如下
    在这里插入图片描述
  3. 第三种和第二种类似

在使用时32位三种结构都可以用,但是64位只能用第三种
由于radix在插入时不会更改结构,且查询时有central cache的桶锁在,不会有线程刚好更改Span所在位置,所以无需加锁,大大提高了性能

9. tcmalloc替换malloc

我们能否替换到系统调用malloc呢?

不同平台替换方式不同。 基于unix的系统上的glibc,使用了weak alias的方式替换。具体来说是因为这些入口函数都被定义成了weak symbols,再加上gcc支持 alias attribute,所以替换就变成了这种通用形式
void* malloc(size_t size) THROW attribute__ ((alias (tc_malloc)))
因此所有malloc的调用都跳转到了tc_malloc的实现

具体可以参考GCC attribute 之weak,alias属性

有些平台不支持这样的东西,需要使用hook的钩子技术来做.
关于hook请看这里:hook

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

墨染萧然

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值