高并发内存池项目

简单介绍相关概念

首先要引入进程,线程,互斥锁这几个概念,我们用生活中的例子来举例。进程就好比工厂中的车间。一个车间可以有很多工人,又要引入一个新的概念,线程,线程好比一个个工人,他们共同完成一个任务。这就意味着一个进程可以包括很多线程。进程空间是被线程共享的。 这时候要引入一个新的概念互斥锁,好比工厂中一些特殊的房间,当在被使用时就要上锁以防止其他人的访问,当不用时又要解锁,线程也是同理。

项目介绍

这个项目一共会创建如下文件:

Common.h

ThreadCache.h

ObjectPool.h

ConcurrentAlloc.h

CentralCache,h

PageCache.h

PageMap.h

ThreadCache.cpp

UnitTest.cpp

CentralCache.cpp

PageCache.cpp

Benchmark.cpp

(下面的代码是在讲解中不断优化添加的)

项目基础:C/C++,数据结构,线程理解,线程锁

什么是内存池

高并发内存池说简单点就是可以在多线程情况下的内存管理相较于malloc更加高效。现在知名的原型就是谷歌开源项目tcmalloc,我们将要实现的是一个简化版。实现对malloc/free相关函数的替换。

首先我们要提到池化技术,我们可能在很多领域都听说过池化技术,比如说在图像识别中,是将图像变成更小的特征图,但却保存原有的图像信息。在内存池中的池化技术是一种内存管理技术,其向操作系统申请一块较大的内存,当程序需要内存时直接向内存池中申请,而不是向操作系统中申请,当程序释放内存时,并不是直接返回给操作系统,而是返还给内存池。除了内存池像对象池,线程池等也是相似原理。

1.因为频繁的申请内存,会多次进行系统调用,就会涉及用户态到内存态再到用户态的转换,开销巨大,内存池就可以实现在用户态对内存进行管理。

2.频繁的向操作系统进行内存申请和释放,会导致不少的很小的内存未被有效利用的情况,因为这些小内存可能是不连续的,不能直接合成一块较大内存进行利用,导致内存碎片的问题。内存池就可以有效解决这些问题。(内存碎片也分内碎片和外碎片,会在下面进行解释,这里提到的就是外碎片)

我们可以将内存池与生活中的一些现象联系起来,比如说买零食,我们可以选择一次只买一点,有需要的时候又自己去超市买,但是这样就会花费不少时间在赶往超市,第二种方法就是第一次就买自己未来一个月所需要的零食,放在家里,这样每次去的时候就方便快速很多。 高并发内存池在多线程的情况下更有优势。

首先实现一个定长内存池

我们要先知道一个单位换算1kb=1024b,1b=8比特。

在我们学习动态链表之前肯定是先了解静态链表,这里我们也先实现一个定长内存池。

这个char* _memmory是指向大块的内存池,这里为什么设置为char*,因为若设置为int*或其它类型不方便管理,char*是一字节,方便进行移动定位(因为要定位分出去后剩下内存池的起始地址)。若被分出去的内存进行了释放归还时应用一个_freeList自由链表先进行管理,用前一个内存块前面四个字节(32位平台)储存后面一个内存的地址方便连接

在内存对象归还给自由链表连接过程中,可能存在空和非空两种情况,但是合理运用头插可用相同代码处理。

第一张是自由链表还是空的情况,第二张是已经悬挂节点(返回了内存对象)的情况。 

        obj->~T();//显示调用析构函数清理对象
		//这里进行头插可以兼顾自由链表为空的情况
		*(void**)obj = _freeList;
		_freeList = obj;

这个代码十分关键,void**二级指针解引用是看一级指针的大小,32位下是4字节,64位下是8字节,就可以直接处理不同向后移动空间的情况。 (只要是二级指针都可以)

当大块内存没有内存对象时会优先向自由链表申请,让自由链表切出相应大小的内存对象,若自由链表也没有足够大小的内存时,可以直接向系统申请内存,windows直接向内存申请内存的函数VirtualAlloc。

ObjectPool.h

#pragma once
#include <iostream>
#include<vector>
#include<time.h>
using std::cout;
using std::cin;//部分展开std
//下面要处理在不同环境下编译的情况
#ifdef _WIN32
#include<windows.h>
#else
//
#endif
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
	void* ptr = VirtualAlloc(0, kpage<<13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);//移位操作更加高效,左移是乘
#else
//linux下brk mmap等
#endif
	if (ptr == nullptr)
		throw std::bad_alloc();
	return 0;
}
template<class T>//表示这个内存池每次都是获取一个T对象
class ObjectPool
{
public:
	T* New()//New个T的对象
	{
		
		T* obj = nullptr;//记录拿走内存的地址

		if (_freeList)//先把还回来的内存优先利用
		{
			void* next = *((void**)_freeList);//这里储存的是地址
			obj =(T*) _freeList;
			_freeList = next;
		

		}
		else
		{
			if (_remainBytes < sizeof(T))//最终分出去剩下的内存不够给一个对象T,直接不要了,重新开大块空间
			{
				_remainBytes=128 * 1024;//若一开始没有内存先分给128kb内存
				_memory = (char*)malloc(_remainBytes);
				if (_memory == nullptr)//处理开空间失败
				{
					printf("malloc is fail");
					exit(-1);
					//throw bad_alloc();//C++的抛异常
				}
			}
			obj = (T*)_memory;
			size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*): sizeof(T);
			_memory +=objSize;//这里不能+=sizeof(obj)因为obj是指针,固定大小是4或者8字节。T的作用就是确定多少内存储存一个新对象
			_remainBytes -= sizeof(T);//每用一次就要减一下
			
		}
		new(obj)T;//显示调用初始化
		return obj;
	}
	void* Delete(T* obj)
	{
		obj->~T();//显示调用析构函数清理对象
		//这里进行头插可以兼顾自由链表为空的情况
		*(void**)obj = _freeList;
		_freeList = obj;
		return obj;
	}
private:
	char* _memory = nullptr;//因为最开始的状态是_memmory,_freeList都是空
	size_t _remainBytes = 0;//指向大块内存被分出去剩余的字节数



	void* _freeList = nullptr;//还回来时先挂在自由链表上

};
//还有一段这个测试效率的代码,并不影响运行

 在release情况下

 

可以看出内存池效率明显有提高

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

在多线程编程下,锁竞争是使性能过多消耗的常见问题。如何优化锁竞争是高并发内存池的目标

总体分为三个大部分

thread cache(线程缓存):用于小内存的分配,有一个线程就要创建一个thread cache ,所以说每一个线程都会独享一个cache,这样线程在这里申请内存的时候就不用加锁,大大提高效率。

central cache(中心缓存);因为中心缓存是共享的,所以thread cache内存不够的时候可以来这里申请内存。中心缓存的结构是哈希桶,在central cache中是存在桶锁的。只有当不同线程因为内存不够访问同一个桶的时候才会存在锁竞争,所以说并不会很激烈。

page cache(页缓存):当CentralCache没有内存对象时。PageCache会分出一定数量的page,切割成小块内存,分给CentralCache,当满足一定条件后,PageCache会回收CentralCache满足条件的span对象,组成更大的页,处理内存碎片问题。

ThreadCache整体设计

我们这里考虑的内存大小通常时256kb及以下。

我们上面介绍的就是一个256kb以内情况下定长内存池的处理,用了一种自由链表,但是却只能解决某一字节数的内存池,还会有其它字节数的长度的情况,所以说我们要考虑搞多个自由链表,采用的哈希桶的结构,但是如果我们每个字节数都考虑弄一个自由链表,消耗就太大了,所以我们要考虑平衡的牺牲一下一定内存。进行如下处理,分别依次挂8,16,24,......,256(如果小于8,用8字节。在8到16之间挂16字节。以此类推),这样导致的浪费就是内碎片。当被申请内存时,首先看对应大小的桶上是否悬挂有自由链表,有的话就取,没有的话则向中心缓存申请。

接下来我们先把ThreadCache.h的相关函数申明写好,再完成自由链表的常见操作。(如头上头插一个内存对象,头上取一个内存对象) 

Common.h

#pragma once
#include <iostream>
#include<vector>
#include<time.h>
#include<assert.h>

using std::cout;
using std::cin;//部分展开std

static void*& NextObj(void* obj)
{
	return *(void**)obj;
}
//管理切分好的小对象的自由链表
class FreeList
{
public:
	void Push(void* obj)
	{
		//头插
		assert(obj);
		NextObj(obj) = _freeList;
		_freeList = obj;
	}
	void* Pop()//拿出头上这个对象
	{
		//头删
		assert(_freeList);
		void* obj = _freeList;
		_freeList = NextObj(obj);
        return obj;
	}
	bool Empty()
	{
		return _freeList == nullptr;
	}

private:
	void* _freeList= nullptr;
};

ThreadCache.h

#include"Common.h"
class ThreadCache
{
public:
	//申请和释放内存对象
	void* Allocate(size_t size);
	void Deallocate(void* ptr, size_t size);//释放时既要知道释放的地址,又要知道大小,因为才知道映射在哪个桶
	
private:
	FreeList _freeList[];//根据映射关系创建数组
};

 ThreadCache哈希桶映射对齐规则

从上面可以看出Thread Cache是哈希桶结构,每来一个大小都要一个桶进行映射,上面我们说过在一个范围内都给一个固定大小,减少消耗。如果我们申请一个内存大小,ThreadCache要给多少大小给我们呢?我们用一个SizeCalss这个类来管理映射规则。我们至少要用八字节用来对齐,因为我们需要一部分内存大小来储存下一个节点地址(地址在不同情况下分别占四字节或八字节)

我们为了降低消耗采取以下对齐方式(若为130字节,我们采取16字节对齐,所以我们要144字节,浪费144-130=14字节)

// [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)

                                                     Common.h          

static inline size_t _RoundUp(size_t bytes, size_t alignNum)

实现找到向上对齐的字节数

static inline size_t _Index(size_t bytes, size_t align_shift)

找到哈希桶对应坐标 

#pragma once
#include <iostream>
#include<vector>
#include<time.h>
#include<assert.h>

using std::cout;
using std::cin;//部分展开std
static const size_t MAX_BYTES = 256 * 1024;
static const size_t NFREE_LIST =208;//208个桶

static void*& NextObj(void* obj)
{
	return *(void**)obj;
}
//管理切分好的小对象的自由链表
class FreeList
{
public:
	void Push(void* obj)
	{
		//头插
		assert(obj);
		NextObj(obj) = _freeList;
		_freeList = obj;
	}
	void* Pop()
	{
		//头删
		assert(_freeList);
		void* obj = _freeList;
		_freeList = NextObj(obj);
        return obj;
	}
	bool Empty()
	{
		return _freeList == nullptr;
	}

private:
	void* _freeList= nullptr;
};
//下面是由tcmalloc简化而来的映射规则
class SizeClass//计算对象大小的对齐映射规则
{
public:
// 整体控制在最多10%左右的内碎片浪费
// [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)
static inline size_t _RoundUp(size_t bytes, size_t alignNum)
{
	/*
	size_t alignSize;
	if(bytes%alignNum!=0)
	{alignSize=(bytes/alignNum+1)*alignNum;}//例如是7字节,7/8=0,0+1=1,1*8=8
	else
	{alignSize=bytes;}
	return alignSize;
	*/
	return (((bytes)+alignNum - 1) & ~(alignNum - 1));//高级方法
}
 // 对齐大小计算
 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 _RoundUp(bytes, 1 << PAGE_SHIFT);
		 return -1;
	 }
	
		 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;
 }
 
};

ThreadCache.cpp

#include"ThreadCache.h"
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 (!_freeList[index].Empty())//如果自由链表有东西则直接取,没有对象则向下一层申请
	{
		return _freeList[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);
	_freeList[index].Push(ptr);
}

ThreadCache.h

#pragma once
#include"Common.h"
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 _freeList[NFREE_LIST];//根据映射关系创建数组
};

ThreadCacheTLS无锁访问

线程局部存储(TLS),是一种变量的存储方法,每一个线程都有一个ThreadCache,线程内的变量在它所在的线程内是全局可访问的,但是不能被其他线程访问到,这样就保持了数据的线程独立性。而熟知的全局变量,是所有线程都可以访问的,这样就不可避免需要锁来控制,增加了控制成本和代码复杂度。

每一个线程都有一个这个变量

static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;

接下来用代码实现每一个线程申请和释放,通过每一个线程拥有一个ThreadCache来避免锁的使用 

ConcurrentAlloc.h

#pragma once

#include "Common.h"
#include "ThreadCache.h"

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

ThreadCache.cpp

#include"ThreadCache.h"
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 (!_freeList[index].Empty())
	{
		return _freeList[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);
	_freeList[index].Push(ptr);
}

 ThreadCache.h

#pragma once
#include"Common.h"
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 _freeList[NFREE_LIST];//根据映射关系创建数组
};
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;//_declspec(thread)是一个特有的拓展,用于申明一个变量的局部存储,若有三个线程就有三个pTLSThreadCache变量

 UnitTest.cpp

#include"ObjectPool.h"
#include "ConcurrentAlloc.h"

void Alloc1()
{
	for (size_t i = 0; i < 5; ++i)
	{
		void* ptr = ConcurrentAlloc(5);
	}
}
void Alloc2()
{
	for (size_t i = 0; i < 5; ++i)
	{
		void* ptr = ConcurrentAlloc(7);
	}
}
void TLSTest()
{
	//这样写可以看到每个线程都有自己的tls
	std::thread t1(Alloc1);//这是库里面的调用方法。创建一个线程调用
	t1.join();
	std::thread t2(Alloc2);
	t2.join();
}
int main()
{
	//TestObjectPool();
	TLSTest();
	
	return 0;
}

前面是线程号后面是地址 

这样可以看出每个线程都有自己独立的TLS。

CentralCache的整体设计

从ThreadCache内存不够时会向中心缓存申请内存,CentralCache也是哈希桶结构不过却与ThreadCache有一些区别,ThreadCache上挂的是一系列内存块,CentralCache的每个哈希桶位是挂的是一个SpanList链表结构,这意味着在这个链表里,存储的是一系列的Span(以页为单位的大块内存),例如8Byte映射位置下面挂的span中的页被切成8Byte大小的对象的自由链表。16KB位置的span中的页被切成16KB大小对象的自由链表,CentralCache映射的spanlist中所有span的都没有内存以后,则需要PageCache申请一个新的span对象.拿到spanl以后将span管理的内存技大小切好作为自由链表链接到一起。若ThreadCache向中心缓存申请内存,中心缓存从span中取对象给ThreadCache。不同字节对应的自由链表所在的桶存在桶锁,CentralCache中挂的span中use_count记录分配了多少个对象出去,分配一个对象给ThreadCache,就++use_count。释放内存,当ThreadCache过长或者线程销毁,则会将内存释放回CentralCache中的,释放回来时-use_count。当use_count减到0时则表示所有对象都回到了span,则将span释放回Page Cache,PageCache中会对前后相邻的空闲页进行合并。

 CentralCache结构设计

主要实现用带头双向循环链表挂span

Common.h

//包含各种头文件
#ifdef _WIN64
typedef unsigned long long PAGE_ID;
#elif _WIN32
typedef size_t PAGE_ID;
#endif 
//省略之前的代码
//管理多个连续页的大块内存跨度,放common.h因为CentralCache和PageCache都要用
struct Span
{
	PAGE_ID _pageId=0;//大块内存起始页的页号
	size_t _n=0;//页的数量
	Span* _next=nullptr;//双向链表的结构
	Span* _prev = nullptr;
	size_t _useCount=0;//切好的小块内存分给ThreadCache的计数
	void* _freeList = nullptr;//切好的小块内存的自由链表

};
//带头双向循环链表
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;
	std::mutex _mtx;//桶锁
};

CentralCache.h

#pragma once

#include "Common.h"


class CentralCache
{

private:
	SpanList _spanLists[NFREE_LIST];//这里的结构和ThreadCache相似


};

CentralCache的核心实现

这里ThreadCache没有内存的时候会不断向CentralCache申请内存,虽然存在桶锁,但是一般是不同的桶锁,竞争还不算大,但是若申请同一个锁,竞争情况就相对较大,效率就会损失。我们可以通过一次多给一些内存来解决这个问题。避免反复进行锁的相关操作。但是一次性给多少内存呢,若要一个一次性给十个或多个,则用完一个,申请后面九个或多个的时候就是无锁的情况。在这种情况下也可能存在不够九个或者多个的情况,这种情况下是有多少个拿多少个,因为目标只是需要一个内存对象,多要的只是方便给ThreadCache后,需要的内存对象可以直接在ThreadCache中申请,避免过多的消耗,这种多给的方法会存在用不完浪费的情况。所以较官方的方法用了一个慢开始反馈调节算法。

慢开始反馈调节算法:还是开始给一个批量内存,大的对象少给一点,小的对象多给一点

Common.h

​
 //......
// 一次ThreadCache从CentralCache获取多少个
 static size_t NumMoveSize(size_t size)
 {
	 assert(size > 0);//size单个对象大小

	 // [2, 512],一次批量移动多少个对象的(慢启动)上限值
	 // 小对象一次批量上限高
	 // 小对象一次批量上限低
	 int num = MAX_BYTES / size;
	 if (num < 2)
		 num = 2;

	 if (num > 512)
		 num = 512;

	 return num;
 }
};
//......

​

ThreadCache.cpp

注意这行代码的理解

min(_freeList[index].MaxSize(),SizeClass::NumMoveSize(size));

一开始不会向CentralCache批量要太多,因为要太多可能用不完,如果不断有size大小的内存的需求,那么batchNum就会不断增长,直到上限
size越大,一次向central cache要的batchNum就越小.size越小,一次向central cache要的batchNum就越大 

#include"ThreadCache.h"
#include"CentralCache.h"
void* ThreadCache::FetchFromCentralCache(size_t index,size_t size)
{
	//慢开始的调节算法
    //一开始不会向CentralCache批量要太多,因为要太多可能用不完,如果不断有size大小的内存的需求,那么batchNum就会不断增长,直到上限
	//size越大,一次向central cache要的batchNum就越小.size越小,一次向central cache要的batchNum就越大
	size_t batchNum = std::min(_freeList[index].MaxSize(),SizeClass::NumMoveSize(size));
	if (_freeList[index].MaxSize() == batchNum)
	{
		_freeList[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
	{
		_freeList[index].PushRange(NextObj(start), end);
		return start;
	}
	

}

CentralCache.cpp

#include"CentralCache.h"
CentralCache CentralCache::_sInst;
Span* CentralCache::GetOneSpan(SpanList& list, size_t byte_size)//获取一个非空Span
{
	//...
	return nullptr;
}
//从中心缓存获取一定数量的对象给Thread Cache
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size)
{
	size_t index = SizeClass::Index(size);//算出对应在桶什么位置
	_spanLists[index]._mtx.lock();
	Span* span = GetOneSpan(_spanLists[index], size);
	assert(span);
	assert(_freeList);
	// 从span中获取batchNum个对象
	// 如果不够batchNum个,有多少拿多少
	size_t i = 0;
	size_t actualNum = 1;
	while (i < batchNum - 1 && NextObj(end) != nullptr)
	{
		end = NextObj(end);
		++i;
		++actualNum;
	}
	span->_freeList = NextObj(end);
	NextObj(end) = nullptr;
	span->_useCount += actualNum;

	_spanLists[index]._mtx.unlock();

	return actualNum;
}

CentralCache.h

#pragma once

#include "Common.h"

// 单例模式
class CentralCache
{
public:
	static CentralCache* GetInstance()
	{
		return &_sInst;//获取实例对象
	}

	// 获取一个非空的span
	Span* GetOneSpan(SpanList& list, size_t byte_size);

	// 从中心缓存获取一定数量的对象给thread cache
	size_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size);

private:
	SpanList _spanLists[NFREE_LIST];//这里的结构和ThreadCache相似

private:
	CentralCache()
	{}

	CentralCache(const CentralCache&) = delete;

	static CentralCache _sInst;
};

Common.h

 //......省略上面的代码
static size_t NumMoveSize(size_t size)
 {
	 assert(size > 0);

	 // [2, 512],一次批量移动多少个对象的(慢启动)上限值
	 // 小对象一次批量上限高
	 // 小对象一次批量上限低
	 int num = MAX_BYTES / size;
	 if (num < 2)
		 num = 2;

	 if (num > 512)
		 num = 512;

	 return num;
 }
};
//管理多个连续页的大块内存跨度,放common.h因为CentralCache和PageCache都要用
struct Span
{
	PAGE_ID _pageId=0;//大块内存起始页的页号
	size_t _n=0;//页的数量
	Span* _next=nullptr;//双向链表的结构
	Span* _prev = nullptr;
	size_t _useCount=0;//切好的小块内存分给ThreadCache的计数
	void* _freeList = nullptr;//切好的小块内存的自由链表

};
//带头双向循环链表
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;//桶锁
};

PageCache整体设计

span是管理多个对象的页。当CentralCache没有span或空闲的span,则要向下一层取对象,PageCache也是哈希桶,对应位置挂的也是一个个span,但是却有不一样,ThreadCache和ThreadCache映射规则都一样,但是PageCache不一样,它挂有128页span,每个页对应一个桶,中心缓存中把页切成小对象给ThreadCache用,用不完可以还给中心缓存,但PageCache不进行切分,中心缓存向页缓存要span的时候要先算好要几页span,又要找到这个span在页缓存的哪个桶,直接给,又因为每一个页对应一个桶所以相对容易找到,也容易还。

 我们可以从上面的图看出来,最大的是128page,为什么最大的是128page呢?从上面可知,我们假设最大申请的单个对象是256kb,128*8k=1024kb=4*256kb,可以看出来对于是单个对象256kb的四倍,完全够用了。只要能满足自己的需求想设置为多少就设置为多少。

PageCache不是桶锁,因为与下面的分裂与合并有关系。当进行跨桶进行合并与切割时,会进行反复的上锁与解锁的操作,大大加大性能消耗要用一个大锁锁起来。

这里会存在一个问若中心缓存向页缓存申请两页的span,若两页的位置没有span,并不是直接向堆申请,而是会向大页申请span,切出两页的span。若后面没有一个挂span,则向系统(堆)申请一个128页的span,挂到对应位置,然后切出一个两页的span拿给中心缓存用,再把剩余的126页span挂到对应位置。

当所以内存还回来的时候要进行合并,如果中心缓存中的span _useCount等于0说明切中心缓存给他、ThreadCache小快内存都回来了,则中心缓存把这个span还给页缓存,页缓存通过页号,查看前后的相邻页是否空闲,是的话就合并,合并出更大的页,解决内存碎片问题。

static const size_t NPAGES = 129;//大小调大一个,方便后面下标处理

PageCache.h

#pragma once
#include "Common.h"
class PageCache
{
public:
	static PageCache* GetInstance()
	{
		return &_sInst;//获取实例对象
	}
	Span* Newspan(size_t k);//获取一个k页的span
private:
	SpanList _sapnLists[NPAGES];
	std::mutex _pageMtx;//大锁

	PageCache()
	{}
	PageCache(const PageCache&) = delete;

	static PageCache _sInst;

};

PageCache.cpp 

#include"PageCache.h"
PageCache PageCache::_sInst;

PageCache中获取span上

先继续完善带头双向循环链表

common.h

//......
class FreeList
{
//......
 // 一次ThreadCache从CentralCache获取多少个
 static size_t NumMoveSize(size_t size)
 {
	 assert(size > 0);

	 // [2, 512],一次批量移动多少个对象的(慢启动)上限值
	 // 小对象一次批量上限高
	 // 小对象一次批量上限低
	 int num = MAX_BYTES / size;
	 if (num < 2)
		 num = 2;

	 if (num > 512)
		 num = 512;

	 return num;
 }
 // 计算一次向系统获取几个页
// 单个对象 8byte
 // ...
 // 单个对象 256KB
 static size_t NumMovePage(size_t size)
 {
	 size_t num = NumMoveSize(size);
	 size_t npage = num * size;
	 npage >>= PAGE_SHIFT;
	 if (npage == 0)
		 npage = 1;
	 return npage;
 }
};
//管理多个连续页的大块内存跨度,放common.h因为CentralCache和PageCache都要用
struct Span
{
	PAGE_ID _pageId=0;//大块内存起始页的页号
	size_t _n=0;//页的数量
	Span* _next=nullptr;//双向链表的结构
	Span* _prev = nullptr;
	size_t _useCount=0;//切好的小块内存分给ThreadCache的计数
	void* _freeList = nullptr;//切好的小块内存的自由链表

};
//带头双向循环链表
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* span)//头插
    {
	Insert(Begin(), span);
    }
	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* CentralCache::GetOneSpan(SpanList& list, size_t size){
//......
}

先从这个函数引入相关内容,ThreadCache向中心缓存申请内存对象 

Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{
	// 查看当前的spanlist中是否有还有未分配对象的span
	Span* it = list.Begin();
	while (it != list.End())
	{
		if (it->_freeList != nullptr)
		{
			return it;
		}
		else
		{
			it = it->_next;
		}
	}

	// 先把central cache的桶锁解掉,这样如果其他线程释放内存对象回来,不会阻塞
	list._mtx.unlock();

	// 走到这里说没有空闲span了,只能找page cache要
    //......

    走到这里说没有空闲span了,只能找PageCache要。

PageCache中获取span下

static const size_t PAGE_SHIFT = 13;

这个代表相关页的转换除以8k等于>>13位  

//计算一次向系统获取几个页
static size_t NumMovePage(size_t size)
 {
	 size_t num = NumMoveSize(size);
	 size_t npage = num * size;
	 npage >>= PAGE_SHIFT;
	 if (npage == 0)
		 npage = 1;
	 return npage;
 }

通过这个函数算出页数。接下来GetOneSpan的逻辑在CentralCache.cpp中接着实现。 

为了实现在不同环境下编译,我们进行条件编译

Common.h

#include <assert.h>

#include <thread>
#include <mutex>
#include <atomic>

using std::cout;
using std::endl;

#ifdef _WIN32
#include <windows.h>
#else
// ...
#endif

static const size_t MAX_BYTES = 256 * 1024;
static const size_t NFREELIST = 208;
static const size_t NPAGES = 129;
static const size_t PAGE_SHIFT = 13;

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

//......
class FreeList
{
//......
 // 一次ThreadCache从CentralCache获取多少个
 static size_t NumMoveSize(size_t size)
 {
	 assert(size > 0);

	 // [2, 512],一次批量移动多少个对象的(慢启动)上限值
	 // 小对象一次批量上限高
	 // 小对象一次批量上限低
	 int num = MAX_BYTES / size;
	 if (num < 2)
		 num = 2;

	 if (num > 512)
		 num = 512;

	 return num;
 }
 // 计算一次向系统获取几个页
// 单个对象 8byte
 // ...
 // 单个对象 256KB
 static size_t NumMovePage(size_t size)
 {
	 size_t num = NumMoveSize(size);
	 size_t npage = num * size;
	 npage >>= PAGE_SHIFT;
	 if (npage == 0)
		 npage = 1;
	 return npage;
 }
};
//管理多个连续页的大块内存跨度,放common.h因为CentralCache和PageCache都要用
struct Span
{
	PAGE_ID _pageId=0;//大块内存起始页的页号
	size_t _n=0;//页的数量
	Span* _next=nullptr;//双向链表的结构
	Span* _prev = nullptr;
	size_t _useCount=0;//切好的小块内存分给ThreadCache的计数
	void* _freeList = nullptr;//切好的小块内存的自由链表

};
//带头双向循环链表
class SpanList
{
public:
	SpanList()
	{
		_head = new Span;
		_head->_next = _head;
		_head->_prev = _head;
	}
	Span* Begin()//遍历
	{
		return _head->_next;
	}
	Span* End()
	{
		return _head;
	}
    bool Empty()
    {
	return _head->_next == _head;
    }
    Span* PopFront()//取出来
    {
	Span* front = _head->_next;
	Erase(front);
	return front;
    }
    void PushFront(Span* span)//头插
    {
	Insert(Begin(), span);
    }
	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;//桶锁
};

char* start = (char*)(span->_pageId << PAGE_SHIFT);//找到页的起始地址,左移是乘
size_t bytes = span->_n << PAGE_SHIFT;//计算大块内存的字节数

这里是通过页号算出起始地址。 又得到单个的大小,就可以大块内存切成自由链表挂起来。

CentralCache.cpp

#include"CentralCache.h"
#include"PageCache.h"

CentralCache CentralCache::_sInst;
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)//获取一个非空Span
{
    //查看当前的spanlist中是否还有未分配对象的span
    Span* it = list.Begin();
    while (it != list.End())//使用迭代器遍历
    {
        if (it->_freeList != nullptr)
        {
            return it;
        }
        else {
            it = it->_next;
        }
    }

    //先把中心缓存的桶锁解掉,这样如果其它线程释放内存对象回来不会阻塞
    list._mtx.unlock();

    //走到这里说明没有空闲的span,只能找页缓存
    PageCache::GetInstance()->_pageMtx.lock();
    Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));
    PageCache::GetInstance()->_pageMtx.unlock();

    //对获取的span切分,不需要加锁,因为其它线程拿不到span


    //处理切分逻辑
    char* start = (char*)(span->_pageId << PAGE_SHIFT);//找到页的起始地址,左移是乘
    size_t bytes = span->_n << PAGE_SHIFT;//计算大块内存的字节数
    char* end = start + bytes;
    //把大块内存切成自由链表链接起来

    //先切一块下来去做头,方便尾插
    span->_freeList = start;
    start += size;
    void* tail = span->_freeList;
    int i = 1;
    while (start < end)
    {
        ++i;
        NextObj(tail) = start;
        tail = NextObj(tail); // tail = start;
        start += size;
    }
    NextObj(tail)=nullptr;
    //切好span后,需要把span挂到桶里面去的时候,再加锁
    list._mtx.lock();
    list.PushFront(span);

    return span;
}
//从中心缓存获取一定数量的对象给Thread Cache
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size)
{
    size_t index = SizeClass::Index(size);//算出对应在桶什么位置
    _spanLists[index]._mtx.lock();
    Span* span = GetOneSpan(_spanLists[index], size);
    assert(span);
    assert(_freeList);
    // 从span中获取batchNum个对象
    // 如果不够batchNum个,有多少拿多少
    size_t i = 0;
    size_t actualNum = 1;
    while (i < batchNum - 1 && NextObj(end) != nullptr)
    {
        end = NextObj(end);
        ++i;
        ++actualNum;
    }
    span->_freeList = NextObj(end);
    NextObj(end) = nullptr;
    span->_useCount += actualNum;

    _spanLists[index]._mtx.unlock();

    return actualNum;
}

因为在PageCache中是通过页数作为桶号进行映射获取span的,所以是几号桶就获取几页的span.在PageCache获取span给中心缓存,若获取两页的span则在页缓存两页的位置找,若找的到则头删出去拿出来,没有就往大页找,例如在三页的位置有span则拿来用,先切分成两页的span和一页的span,两页的拿去用,剩下的一页挂在一页的位置上。在NewSpan函数中实现 

PageCache.cpp

#include"PageCache.h"
PageCache PageCache::_sInst;
//获取一个k页的span
Span* PageCache::NewSpan(size_t k)
{
	assert(k > 0 && k < NPAGES);

	if (!_spanLists[k].Empty())//先检查第k个桶有没有span
	{
		return _spanLists->PopFront();//拿出来
	}
	//检查后面的桶里面有没有span,往大页找,例如在三页的位置有span则拿来用,先切分成两页的span和一页的span,两页的拿去给中心缓存用,剩下的一页挂在一页的位置上。
	for (size_t i = k + 1; i < NPAGES; ++i)//从下一页开始找
	{
		if (!_spanLists[i].Empty())
		{
			//开始切
			Span* nSpan = _spanLists[i].PopFront();
			Span* kSpan = new Span;
			//在nSpan的头部切一个k页下来,k页返回,nSpan挂到对应映射位置
			kSpan->_pageId = nSpan->_pageId;
			kSpan->_n = k;
			nSpan->_pageId += k;
			nSpan->_n -= k;
			//处理剩下页数的span
			_spanLists[nSpan->_n].PushFront(nSpan);
			return kSpan;
		}
	}
	//走到这个位置说明后面没有大页的span了,这时向系统(堆)要一个128页的span
	Span* bigSpan = new Span;
	void* ptr = SystemAlloc(NPAGES - 1);
	bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;//有地址算页号就是除
	bigSpan->_n =NPAGES-1;
	_spanLists[bigSpan->_n].PushFront(bigSpan);
	return NewSpan(k);

	
}
	return NewSpan(k);

 这里类似递归重新调用了自己,但不会进入死循环,因为这里至少有了128页可以进行切分,就会在for循环里面找满足调节,不会再一次调用NewSpan()了。

PageCache.h

#pragma once
#include "Common.h"
class PageCache
{
public:
	static PageCache* GetInstance()
	{
		return &_sInst;//获取实例对象
	}
	Span* NewSpan(size_t k);//获取一个k页的span
	std::mutex _pageMtx;
private:
	SpanList _spanLists[NPAGES];//NPAGES=128
	

	PageCache()
	{
	}
	PageCache(const PageCache&) = delete;

	static PageCache _sInst;

}; 

UnitTest.cpp

#include"ObjectPool.h"
#include "ConcurrentAlloc.h"


void TestConcurrentAlloc()
{
	void* p1 = ConcurrentAlloc(6);
	void* p2= ConcurrentAlloc(8);
	void* p3 = ConcurrentAlloc(1);
	void* p4 = ConcurrentAlloc(7);
	void* p5 = ConcurrentAlloc(8);
	cout << p1 << std::endl;
	cout << p2 << std::endl;
	cout << p3 << std::endl;
	cout << p4 << std::endl;
	cout << p5 << std::endl;
}
int main()
{
	//TestObjectPool();
	//TLSTest();
	TestConcurrentAlloc();
	return 0;
}

这是调用一个并发内存分配的操作,得到内存地址。

可以看出来这些申请的小块内存是连续的 

ThreadCache回收内存

首先因为每一个线程都有一个ThreadCache,所以大多数都是无锁操作,使其更加高效。当线程中某些对象不再使用时会返回到ThreadCache中,再挂到对应的哈希桶上,当某个桶上的自由链表因为释放对象不断增加到某个大小时,就会批量返回给中心缓存的span。

ThreadCache.h

添加

void ListTooLong(FreeList& list, size_t size);

ThreadCache释放内存会挂在自由链表上,当自由链表过长时会回收内存到中心缓存 

ThreadCache .cpp

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

	// 还给桶的时候要知道大小才知道还给那个桶
	size_t index = SizeClass::Index(size);
	_freeList[index].Push(ptr);
	if (_freeList[index].Size()>=_freeList[index].MaxSize())//当链表长度大于一次批量申请的内存时就开始还一段list给中心缓存
	{
		ListTooLong(_freeList[index], size);
	}
}
void* ThreadCache::ListTooLong(FreeList& list, size_t size)
{
	void* start = nullptr;
	void* end = nullptr;
	//取一批内存回来还给下一层
	list.PopRange(start, end, list.MaxSize());//取一次批量的内存

	CentralCache::GetInstance()->ReleaseListToSpans(start, size);
}
//......

CentralCache.h

添加

// 将一定数量的对象释放到span跨度
void ReleaseListToSpans(void* start, size_t byte_size);

Common.h

PopRange图示

 

//......
class FreeList
{
public:
	void Push(void* obj)
	{
		//头插
		assert(obj);
		NextObj(obj) = _freeList;
		_freeList = obj;
		++_size;
	}
	void PushRange(void* start, void* end,size_t n)//给一个范围给多个对象
	{
		NextObj(end) = _freeList;
		_freeList = start;
		_size += n;
	}
	void 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);
		NextObj(end) = nullptr;
		_size -= n;
	}
	void* Pop()
	{
		//头删
		assert(_freeList);
		void* obj = _freeList;
		_freeList = NextObj(obj);
		--_size;
		return obj;
	}
	bool Empty()
	{
		return _freeList == nullptr;
	}
	size_t& MaxSize()
	{
		return _maxSize;
	}
	size_t Size()
	{
		return _size;
	}
private:
	void* _freeList=nullptr;
	size_t _maxSize = 1;
	size_t _size;//记录数据个数
};
//......

CentralCache回收内存

首先ThreadCache中自由链表回收的内存可能是不同对象还回来的。我们要知道哪个对象属于哪个页。

UnitTest.cpp

//......
void TestAddressShift()
{
	PAGE_ID id1 = 2000;
	PAGE_ID id2 = 2001;
	char* p1 = (char*)(id1 << PAGE_SHIFT);
	char* p2 = (char*)(id2 << PAGE_SHIFT);
	while (p1 < p2)
	{
		cout << (void*)p1 << ":" << ((PAGE_ID)p1 >> PAGE_SHIFT) << std::endl;
		p1 += 8;
	}
}
int main()
{
	//TestObjectPool();
	//TLSTest();
	//TestConcurrentAlloc1();
	TestAddressShift();

	return 0;
}

 

这样可以看出2000页的部分范围的地址。

若我们想知道某个内存块在哪个span,所以我们可以通过内存块的地址算出是哪个页,然后知道是哪个span,但因为要遍历每个span,所以时间复杂度较高。我们可以通过建立映射来解决 

PageCache.cpp

//......
Span* PageCache::MapObjectToSpan(void* obj)//给了一个obj(地址)算span
{
	PAGE_ID id = ((PAGE_ID)obj >> PAGE_SHIFT);
	auto ret = _idSpanMap.find(id);
	if (ret != _idSpanMap.end())
	{
		return ret->second;
	}
	else
	{
		assert(false);
		return nullptr;
	}
}
void PageCache::ReleaseSpanToPageCache(Span* span)
{
	
}

PageCache.h

添加

Span* MapObjectToSpan(void* obj);//获取从对象到span的映射
//
void ReleaseSpanToPageCache(Span* span); //释放空闲span回到Pagecache,并合并相邻的span
Span* NewSpan(size_t k);//获取一个k页的span

CentralCache.cpp

实现还给span

//......
void CentralCache::ReleaseListToSpans(void* start, size_t size)
{
    size_t index = SizeClass::Index(size);
    _spanLists[index]._mtx.lock();
    while (start)
    {
        void* next = NextObj(start);

        Span* span = PageCache::GetInstance()->MapObjectToSpan(start);//算出是哪个span
        NextObj(start) = span->_freeList;//把对象头插到span
        span->_freeList = start;
        span->_useCount--;

        // 说明span的切分出去的所有小块内存都回来了 ,这个span就可以再回收给page cache,pagecache可以再尝试去做前后页的合并
        if (span->_useCount == 0)
        {
            _spanLists[index].Erase(span);
            span->_freeList = nullptr;//清理
            span->_next = nullptr;
            span->_prev = nullptr;

            // 释放span给page cache时,使用page cache的锁就可以了
            // 这时把桶锁解掉,如果不解,其它线程想在这个桶申请释放内存操作不了
            _spanLists[index]._mtx.unlock();//这里要解锁不然会影响其它对象返还内存

            PageCache::GetInstance()->_pageMtx.lock();
            PageCache::GetInstance()->ReleaseSpanToPageCache(span);
            PageCache::GetInstance()->_pageMtx.unlock();

            _spanLists[index]._mtx.lock();
        }

        start = next;
    }

    _spanLists[index]._mtx.unlock();
}

 CentralCache.h

添加

//将一定数量的对象释放到span跨度
void ReleaseListToSpans(void* start, size_t byte_size);

PageCache回收内存

当中心缓存span中usecount为0时,则释放回PageCache,再进行页合并,减小内存碎片。这一部分主要说明 ReleaseListToSpans。那么如何拼接内存碎片呢,若还回来的都是一二page的,如何正确拼接成一个大页使其连续可以使用呢?若我的pageId是666为两页,我们若1往前看,看665(若为三页)是否空闲,若空闲,则可以合成五页的span。往后看同理。若合成的页大于128就要停止,因为不知道挂在哪里

PageCache.cpp

#include "PageCache.h"

PageCache PageCache::_sInst;

// 获取一个K页的span
Span* PageCache::NewSpan(size_t k)
{
	assert(k > 0);

	// 大于128 page的直接向堆申请
	if (k > NPAGES - 1)
	{
		void* ptr = SystemAlloc(k);
		//Span* span = new Span;
		Span* span = _spanPool.New();

		span->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
		span->_n = k;

		_idSpanMap[span->_pageId] = span;
		return span;
	}

	// 先检查第k个桶里面有没有span
	if (!_spanLists[k].Empty())
	{
		Span* kSpan = _spanLists[k].PopFront();

		// 建立id和span的映射,方便central cache回收小块内存时,查找对应的span
		for (PAGE_ID i = 0; i < kSpan->_n; ++i)
		{
			_idSpanMap[kSpan->_pageId + i] = kSpan;
		}

		return kSpan;
	}

	// 检查一下后面的桶里面有没有span,如果有可以把他它进行切分
	for (size_t i = k + 1; i < NPAGES; ++i)
	{
		if (!_spanLists[i].Empty())
		{
			Span* nSpan = _spanLists[i].PopFront();
			//Span* kSpan = new Span;
			Span* kSpan = _spanPool.New();

			// 在nSpan的头部切一个k页下来
			// k页span返回
			// nSpan再挂到对应映射的位置
			kSpan->_pageId = nSpan->_pageId;
			kSpan->_n = k;

			nSpan->_pageId += k;
			nSpan->_n -= k;

			_spanLists[nSpan->_n].PushFront(nSpan);
			// 存储nSpan的首位页号跟nSpan映射,方便page cache回收内存时
			// 进行的合并查找
			_idSpanMap[nSpan->_pageId] = nSpan;
			_idSpanMap[nSpan->_pageId + nSpan->_n - 1] = nSpan;


			// 建立id和span的映射,方便central cache回收小块内存时,查找对应的span
			for (PAGE_ID i = 0; i < kSpan->_n; ++i)
			{
				_idSpanMap[kSpan->_pageId + i] = kSpan;
			}

			return kSpan;
		}
	}

	// 走到这个位置就说明后面没有大页的span了
	// 这时就去找堆要一个128页的span
	//Span* bigSpan = new Span;
	Span* bigSpan = _spanPool.New();
	void* ptr = SystemAlloc(NPAGES - 1);
	bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
	bigSpan->_n = NPAGES - 1;

	_spanLists[bigSpan->_n].PushFront(bigSpan);

	return NewSpan(k);
}
void PageCache::ReleaseSpanToPageCache(Span* span)
{
	// 对span前后的页,尝试进行合并,缓解内存碎片问题
	while (1)//因为有条件判断所以一定会退出
	{
		PAGE_ID prevId = span->_pageId - 1;
		auto ret = _idSpanMap.find(prevId);
		// 前面的页号没有,不合并了
		if (ret == _idSpanMap.end())
		{
			break;
		}

		// 前面相邻页的span在使用,不合并了
		Span* prevSpan = ret->second;
		if (prevSpan->_isUse == true)//这里添加了_isUse判断是否在进行使用
		{
			break;
		}

		// 合并出超过128页的span没办法管理也不合并
		if (prevSpan->_n + span->_n > NPAGES - 1)
		{
			break;
		}

		span->_pageId = prevSpan->_pageId;
		span->_n += prevSpan->_n;

		_spanLists[prevSpan->_n].Erase(prevSpan);
		delete prevSpan;
	}

	// 向后合并
	while (1)
	{
		PAGE_ID nextId = span->_pageId + span->_n;
		auto ret = _idSpanMap.find(nextId);
		if (ret == _idSpanMap.end())
		{
			break;
		}

		Span* nextSpan = ret->second;
		if (nextSpan->_isUse == true)
		{
			break;
		}

		if (nextSpan->_n + span->_n > NPAGES - 1)
		{
			break;
		}

		span->_n += nextSpan->_n;

		_spanLists[nextSpan->_n].Erase(nextSpan);
		delete nextSpan;
	}

	_spanLists[span->_n].PushFront(span);
	span->_isUse = false;
	_idSpanMap[span->_pageId] = span;
	_idSpanMap[span->_pageId + span->_n - 1] = span;
}

大于256kb的大块内存问题

当小于256kb时直接找三层缓存,当大于256kb时分两种情况,当32(页)*8k<size<=128(页)*8k时找PageCache,当大于128(页)*8k时找系统(堆)。

Common.h

重新修改一下计算大于256kb的情况

// 对齐大小计算
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 
 {
	 return _RoundUp(bytes, 1<<PAGE_SHIFT);
 }

	 
}
 return _RoundUp(bytes, 1<<PAGE_SHIFT);

按8kb对齐 。

ConcurrentAlloc.h

static void* ConcurrentAlloc(size_t size)
{
	if (size > MAX_BYTES)
	{
		size_t alignSize = SizeClass::RoundUp(size);
		size_t kpage = alignSize >> PAGE_SHIFT;

		PageCache::GetInstance()->_pageMtx.lock();
		Span* span = PageCache::GetInstance()->NewSpan(kpage);//要的是地址
		
		PageCache::GetInstance()->_pageMtx.unlock();

		void* ptr = (void*)(span->_pageId << PAGE_SHIFT);
		return ptr;
	}
	else 
	{
		//每个线程获取自己的ThreadCache对象
		if (pTLSThreadCache == nullptr)
		{
			pTLSThreadCache = new ThreadCache;
		}
		cout << std::this_thread::get_id() << ":" << pTLSThreadCache << std::endl;
		return pTLSThreadCache->Allocate(size);
	}
}

PageCache.cpp

//获取一个k页的span
Span* PageCache::NewSpan(size_t k)
{
	assert(k > 0 && k < NPAGES);

	// 大于128 page的直接向堆申请
	if (k > NPAGES - 1)
	{
		void* ptr = SystemAlloc(k);
		Span* span = new Span;
		

		span->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
		span->_n = k;
		_idSpanMap[span->_pageId] = span;
		
		return span;
	}
	if (!_spanLists[k].Empty())//先检查第k个桶有没有span
	{
		return _spanLists->PopFront();//拿出来
	}
	//检查后面的桶里面有没有span,往大页找,例如在三页的位置有span则拿来用,先切分成两页的span和一页的span,两页的拿去给中心缓存用,剩下的一页挂在一页的位置上。
	for (size_t i = k + 1; i < NPAGES; ++i)//从下一页开始找
	{
		if (!_spanLists[i].Empty())
		{
			//开始切
			Span* nSpan = _spanLists[i].PopFront();
			Span* kSpan = new Span;
			//在nSpan的头部切一个k页下来,k页返回,nSpan挂到对应映射位置
			kSpan->_pageId = nSpan->_pageId;
			kSpan->_n = k;
			nSpan->_pageId += k;
			nSpan->_n -= k;
			//处理剩下页数的span
			_spanLists[nSpan->_n].PushFront(nSpan);

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


			// 建立id和span的映射,方便central cache回收小块内存时,查找对应的span
			for (PAGE_ID i = 0; i < kSpan->_n; ++i)
			{
				_idSpanMap[kSpan->_pageId + i] = kSpan;
			}


			return kSpan;
		}
	}
	//走到这个位置说明后面没有大页的span了,这时向系统(堆)要一个128页的span
	Span* bigSpan = new Span;
	void* ptr = SystemAlloc(NPAGES - 1);
	bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
	bigSpan->_n =NPAGES-1;
	_spanLists[bigSpan->_n].PushFront(bigSpan);
	return NewSpan(k);

	
}

接下来我们要进行释放操作,虽然我们只知道释放对象的起始地址,但我们已经实现了其的映射关系,从而找到span。

PageCache.cpp

void PageCache::ReleaseSpanToPageCache(Span* span)
{
	// 大于128 page的直接还给堆
	if (span->_n > NPAGES - 1)
	{
		void* ptr = (void*)(span->_pageId << PAGE_SHIFT);//借助页号找到指针
		SystemFree(ptr);
		delete span;
	

		return;
	}

	

	// ......
}

Common.h

inline static void SystemFree(void* ptr)
{
#ifdef _WIN32
	VirtualFree(ptr, 0, MEM_RELEASE);
#else
	// sbrk unmmap等
#endif
}

ConcurrentAlloc.h

static void* ConcurrentFree(void* ptr, size_t size)
{
	
	

	if (size > MAX_BYTES)
	{
		Span* span = PageCache::GetInstance()->MapObjectToSpan(ptr);
		PageCache::GetInstance()->_pageMtx.lock();
		PageCache::GetInstance()->ReleaseSpanToPageCache(span);
		PageCache::GetInstance()->_pageMtx.unlock();
	}
	else
	{
		assert(pTLSThreadCache);
		pTLSThreadCache->Deallocate(ptr, size);
	}
}

CentralCache.cpp

void CentralCache::ReleaseListToSpans(void* start, size_t size)
{
    size_t index = SizeClass::Index(size);
    _spanLists[index]._mtx.lock();
    while (start)
    {
        void* next = NextObj(start);

        Span* span = PageCache::GetInstance()->MapObjectToSpan(start);
        NextObj(start) = span->_freeList;
        span->_freeList = start;
        span->_useCount--;

        // 说明span的切分出去的所有小块内存都回来了 ,这个span就可以再回收给page cache,pagecache可以再尝试去做前后页的合并
        if (span->_useCount == 0)
        {
            _spanLists[index].Erase(span);
            span->_freeList = nullptr;//清理
            span->_next = nullptr;
            span->_prev = nullptr;

            // 释放span给page cache时,使用page cache的锁就可以了
            // 这时把桶锁解掉,如果不解,其它线程想在这个桶申请释放内存操作不了
            _spanLists[index]._mtx.unlock();

            PageCache::GetInstance()->_pageMtx.lock();
            PageCache::GetInstance()->ReleaseSpanToPageCache(span);
            PageCache::GetInstance()->_pageMtx.unlock();

            _spanLists[index]._mtx.lock();
        }

        start = next;
    }

    _spanLists[index]._mtx.unlock();
}

使用定长内存池配合脱离使用new

我们使用tcmalloc的就是在某些情况下相对于使用malloc更加的高效,实现替代malloc。那我们在自己实现的tcmalloc中更不能直接调用malloc/new与delete。所以我们要把代码中的malloc/new改为自己实现的代码,delete也改成自己写的。

PageCache.h

添加

ObjectPool<Span> _spanPool;
#pragma once

#include "Common.h"
#include "ObjectPool.h"

class PageCache
{
public:
	static PageCache* GetInstance()
	{
		return &_sInst;
	}

	// 获取从对象到span的映射
	Span* MapObjectToSpan(void* obj);

	// 释放空闲span回到Pagecache,并合并相邻的span
	void ReleaseSpanToPageCache(Span* span);

	// 获取一个K页的span
	Span* NewSpan(size_t k);

	std::mutex _pageMtx;
private:
	SpanList _spanLists[NPAGES];
	ObjectPool<Span> _spanPool;

	//std::unordered_map<PAGE_ID, Span*> _idSpanMap;
	std::map<PAGE_ID, Span*> _idSpanMap;

	PageCache()
	{}
	PageCache(const PageCache&) = delete;


	static PageCache _sInst;
};

再把new/malloc/delete全部换成自己实现的 

 

可以看出来,仍然可以正常运行。

释放对象时优化为不传对象大小的

我们目前实现的回收内存都是要指明大小的,因为要区分是大块内存还是小块内存当小于256kb时直接找三层缓存,当大于256kb时分两种情况,当32(页)*8k<size<=128(页)*8k时找PageCache,当大于128(页)*8k时找系统(堆)。。如果我们不想传大小该怎么操作呢?我们可以用map实现一个映射实现简化。

Common.h

添加

size_t _objSize = 0;  // 切好的小对象的大小
struct Span
{
	PAGE_ID _pageId = 0; // 大块内存起始页的页号
	size_t  _n = 0;      // 页的数量

	Span* _next = nullptr;	// 双向链表的结构
	Span* _prev = nullptr;

	size_t _objSize = 0;  // 切好的小对象的大小
	size_t _useCount = 0; // 切好小块内存,被分配给thread cache的计数
	void* _freeList = nullptr;  // 切好的小块内存的自由链表

	bool _isUse = false;          // 是否在被使用
};

CentralCache.cpp

span->_objSize = size;

把这个大小保存起来。 

Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{
	// 查看当前的spanlist中是否有还有未分配对象的span
	Span* it = list.Begin();
	while (it != list.End())
	{
		if (it->_freeList != nullptr)
		{
			return it;
		}
		else
		{
			it = it->_next;
		}
	}

	// 先把central cache的桶锁解掉,这样如果其他线程释放内存对象回来,不会阻塞
	list._mtx.unlock();

	// 走到这里说没有空闲span了,只能找page cache要
	PageCache::GetInstance()->_pageMtx.lock();
	Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));
	span->_isUse = true;
	span->_objSize = size;
	PageCache::GetInstance()->_pageMtx.unlock();

	// 对获取span进行切分,不需要加锁,因为这会其他线程访问不到这个span

	// 计算span的大块内存的起始地址和大块内存的大小(字节数)
	char* start = (char*)(span->_pageId << PAGE_SHIFT);
	size_t bytes = span->_n << PAGE_SHIFT;
	char* end = start + bytes;

	// 把大块内存切成自由链表链接起来
	// 1、先切一块下来去做头,方便尾插
	span->_freeList = start;
	start += size;
	void* tail = span->_freeList;
	int i = 1;
	while (start < end)
	{
		++i;
		NextObj(tail) = start;
		tail = NextObj(tail); // tail = start;
		start += size;
	}

	NextObj(tail) = nullptr;

	
	// 切好span以后,需要把span挂到桶里面去的时候,再加锁
	list._mtx.lock();
	list.PushFront(span);

	return span;
}

 ConcurrentAlloc.h

static void ConcurrentFree(void* ptr)
{
	Span* span = PageCache::GetInstance()->MapObjectToSpan(ptr);
	size_t size = span->_objSize;

	if (size > MAX_BYTES)
	{
		PageCache::GetInstance()->_pageMtx.lock();
		PageCache::GetInstance()->ReleaseSpanToPageCache(span);
		PageCache::GetInstance()->_pageMtx.unlock();
	}
	else
	{
		assert(pTLSThreadCache);
		pTLSThreadCache->Deallocate(ptr, size);
	}
}

PageCache.cpp

//......
Span* PageCache::MapObjectToSpan(void* obj)
{
	PAGE_ID id = ((PAGE_ID)obj >> PAGE_SHIFT);

	std::unique_lock<std::mutex> lock(_pageMtx);

	auto ret = _idSpanMap.find(id);
	if (ret != _idSpanMap.end())
	{
		return ret->second;
	}
	else
	{
		assert(false);
		return nullptr;
	}
}
//......

多线程环境下对比malloc测试

Benchmark.cpp

ntimes 一轮申请和释放内存的次数,nworks创建多少线程,rounds 轮次

#include"ConcurrentAlloc.h"


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轮次,每轮次malloc %u次: 花费:%u ms\n", (size_t)nworks, (size_t)rounds, (size_t)ntimes, (size_t)malloc_costtime);

	printf("%u个线程并发执行%u轮次,每轮次free %u次: 花费:%u ms\n", (size_t)nworks, (size_t)rounds, (size_t)ntimes, (size_t)free_costtime);

	printf("%u个线程并发malloc&free %u次,总计花费:%u ms\n", (size_t)nworks, (size_t)nworks * rounds * ntimes, (size_t)(malloc_costtime + free_costtime));
}


// 单轮次申请释放次数 线程数 轮次
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",
		(size_t)nworks, (size_t)rounds, (size_t)ntimes, (size_t)malloc_costtime);

	printf("%u个线程并发执行%u轮次,每轮次concurrent dealloc %u次: 花费:%u ms\n",
		(size_t)nworks, (size_t)rounds, (size_t)ntimes, (size_t)free_costtime);

	printf("%u个线程并发concurrent alloc&dealloc %u次,总计花费:%u ms\n",
		(size_t)nworks, (size_t)nworks * rounds * ntimes, (size_t)(malloc_costtime + free_costtime));
}

int main()
{
	size_t n = 1000;
	cout << "==========================================================" << std::endl;
	BenchmarkConcurrentMalloc(n, 4, 10);
	cout << std::endl << std::endl;

	//BenchmarkMalloc(n, 4, 10);
	cout << "==========================================================" << std::endl;

	return 0;
}

PageCache.cpp

#include "PageCache.h"

PageCache PageCache::_sInst;

// 获取一个K页的span
Span* PageCache::NewSpan(size_t k)
{
	assert(k > 0);

	// 大于128 page的直接向堆申请
	if (k > NPAGES-1)
	{
		void* ptr = SystemAlloc(k);
		//Span* span = new Span;
		Span* span = _spanPool.New();

		span->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
		span->_n = k;

		_idSpanMap[span->_pageId] = span;
		return span;
	}

	
	if (!_spanLists[k].Empty())// 先检查第k个桶里面有没有span
	{
		Span* kSpan = _spanLists[k].PopFront();

		// 建立id和span的映射,方便central cache回收小块内存时,查找对应的span
		for (PAGE_ID i = 0; i < kSpan->_n; ++i)
		{
			_idSpanMap[kSpan->_pageId + i] = kSpan;
		}

		return kSpan;
	}

	// 检查后面的桶里面有没有span,往大页找,例如在三页的位置有span则拿来用,先切分成两页的span和一页的span,两页的拿去给中心缓存用,剩下的一页挂在一页的位置上。
	for (size_t i = k+1; i < NPAGES; ++i)
	{
		//开始切
		if (!_spanLists[i].Empty())
		{
			Span* nSpan = _spanLists[i].PopFront();
			//Span* kSpan = new Span;
			Span* kSpan = _spanPool.New();

			//在nSpan的头部切一个k页下来,k页返回,nSpan挂到对应映射位置
			kSpan->_pageId = nSpan->_pageId;
			kSpan->_n = k;

			nSpan->_pageId += k;
			nSpan->_n -= k;
			//处理剩下页数的span
			_spanLists[nSpan->_n].PushFront(nSpan);
			
			_idSpanMap[nSpan->_pageId] = nSpan;
			_idSpanMap[nSpan->_pageId + nSpan->_n - 1] = nSpan;


			// 建立id和span的映射,方便central cache回收小块内存时,查找对应的span
			for (PAGE_ID i = 0; i < kSpan->_n; ++i)
			{
				_idSpanMap[kSpan->_pageId + i] = kSpan;
			}

			return kSpan;
		}
	}

	// 走到这个位置就说明后面没有大页的span了这时就去找堆要一个128页的span
	//Span* bigSpan = new Span;
	Span* bigSpan = _spanPool.New();
	void* ptr = SystemAlloc(NPAGES - 1);
	bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
	bigSpan->_n = NPAGES - 1;

	_spanLists[bigSpan->_n].PushFront(bigSpan);

	return NewSpan(k);
}

Span* PageCache::MapObjectToSpan(void* obj)
{
	PAGE_ID id = ((PAGE_ID)obj >> PAGE_SHIFT);

	std::unique_lock<std::mutex> lock(_pageMtx);

	auto ret = _idSpanMap.find(id);
	if (ret != _idSpanMap.end())
	{
		return ret->second;
	}
	else
	{
		assert(false);
		return nullptr;
	}
}

void PageCache::ReleaseSpanToPageCache(Span* span)
{
	// 大于128 page的直接还给堆
	if (span->_n > NPAGES-1)
	{
		void* ptr = (void*)(span->_pageId << PAGE_SHIFT);
		SystemFree(ptr);
		//delete span;
		_spanPool.Delete(span);

		return;
	}

	// 对span前后的页,尝试进行合并,缓解内存碎片问题
	while (1)
	{
		PAGE_ID prevId = span->_pageId - 1;
		auto ret = _idSpanMap.find(prevId);
		// 前面的页号没有,不合并了
		if (ret == _idSpanMap.end())
		{
			break;
		}

		// 前面相邻页的span在使用,不合并了
		Span* prevSpan = ret->second;
		if (prevSpan->_isUse == true)
		{
			break;
		}

		// 合并出超过128页的span没办法管理,不合并了
		if (prevSpan->_n + span->_n > NPAGES-1)
		{
			break;
		}

		span->_pageId = prevSpan->_pageId;
		span->_n += prevSpan->_n;

		_spanLists[prevSpan->_n].Erase(prevSpan);
		//delete prevSpan;
		_spanPool.Delete(prevSpan);
	}

	// 向后合并
	while (1)
	{
		PAGE_ID nextId = span->_pageId + span->_n;
		auto ret = _idSpanMap.find(nextId);
		if (ret == _idSpanMap.end())
		{
			break;
		}

		Span* nextSpan = ret->second;
		if (nextSpan->_isUse == true)
		{
			break;
		}

		if (nextSpan->_n + span->_n > NPAGES-1)
		{
			break;
		}

		span->_n += nextSpan->_n;

		_spanLists[nextSpan->_n].Erase(nextSpan);
		//delete nextSpan;
		_spanPool.Delete(nextSpan);
	}

	_spanLists[span->_n].PushFront(span);
	span->_isUse = false;
	_idSpanMap[span->_pageId] = span;
	_idSpanMap[span->_pageId+span->_n-1] = span;
}

可以看出来效率还是低于malloc,free。我们可以用VS2022自带的性能分析工具来分析是什么地方消耗时间较长。

我们可以看出来光ThreadCache中Deallocate函数和PageCache中MapObjectToSpan函数加起来就占了一大半时间。

在 ThreadCache中Deallocate函数

可以看出来ListTooLong占用时间最多

可以看出来ReleaseListToSpans占用时间最多 

可以看出来在锁和MapObjectToSpan的调用最多

void CentralCache::ReleaseListToSpans(void* start, size_t size)
{
	size_t index = SizeClass::Index(size);
	_spanLists[index]._mtx.lock();
	while (start)
	{
		void* next = NextObj(start);

		Span* span = PageCache::GetInstance()->MapObjectToSpan(start);
		NextObj(start) = span->_freeList;
		span->_freeList = start;
		span->_useCount--;

		// 说明span的切分出去的所有小块内存都回来了
		// 这个span就可以再回收给page cache,pagecache可以再尝试去做前后页的合并
		if (span->_useCount == 0)
		{
			_spanLists[index].Erase(span);
			span->_freeList = nullptr;
			span->_next = nullptr;
			span->_prev = nullptr;

			// 释放span给page cache时,使用page cache的锁就可以了
			// 这时把桶锁解掉
			_spanLists[index]._mtx.unlock();

			PageCache::GetInstance()->_pageMtx.lock();
			PageCache::GetInstance()->ReleaseSpanToPageCache(span);
			PageCache::GetInstance()->_pageMtx.unlock();

			_spanLists[index]._mtx.lock();
		}

		start = next;
	}

	_spanLists[index]._mtx.unlock();
}

可以看出来因为锁竞争导致过多时间消耗。 

使用基数树进行优化

我们直接借鉴tcmalloc源码中的基数树.因为我们是在x86架构下,所以采取两层基数树,若换成x64架构则必须是三层基数树。

PageMap.h

#pragma once
#include"Common.h"

// 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() {
	}
};

PageCache.h

#pragma once
#include "Common.h"
#include"ObjectPool.h"
#include"PageMap.h"
class PageCache
{
public:
	static PageCache* GetInstance()
	{
		return &_sInst;//获取实例对象
	}

	Span* MapObjectToSpan(void* obj);//获取从对象到span的映射
	//
	void ReleaseSpanToPageCache(Span* span); //释放空闲span回到Pagecache,并合并相邻的span
	Span* NewSpan(size_t k);//获取一个k页的span
	std::mutex _pageMtx;
private:
	SpanList _spanLists[NPAGES];//NPAGES=129
	ObjectPool<Span> _spanPool;
	//std::unordered_map<PAGE_ID, Span* > _idSpanMap;
	//std::unordered_map<PAGE_ID,size_t > _idSizeMap;
	TCMalloc_PageMap1<32-PAGE_SHIFT> _idSpanMap;
	PageCache()
	{}
	PageCache(const PageCache&) = delete;

	static PageCache _sInst;

}; 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值