简易高并发内存池

从零实现一个高并发的内存池

  • 项目介绍

    ​ 项目原型是google的开源项目tcmalloc,即线程缓存的malloc,针对其核心框架做的简易版,实现了高效的多线程内存管理,用于替代系统的内存分配相关的函数malloc和free,主要目的是为了学习tcmalloc的精华。

  • 所需技术:

    ​ C/C++、数据结构(链表、哈希桶)、操作系统内存管理、单例模式、多线程、互斥锁等,开发环境为windows下VS2013,项目中对Linux环境用法也有介绍

  • 什么是内存池

    ​ 内存池指的是程序预先向操作系统申请足够大的一块内存空间;此后,程序中需要申请内存时,不需要直接向操作系统申请,而是直接从内存池中获取;同理,程序释放内存时,也不是将内存直接还给操作系统,而是将内存归还给内存池。

    ​ 当程序退出(或者特定时间)时,内存池才将之前申请的内存真正释放。

  • 调用系统接口申请内存/为什么需要内存池

    在C语言中申请内存用的是malloc函数,当频繁的malloc会有出现以下问题:

    1.碎片问题:

    ​ 例如有10个G,A拿走前3个,B拿走中间3个,C拿走第7、8个,现在空闲9、10。现在B把3个都还了,之后D申请4个,但是B还的3个与空闲的2个不连续,就导致申请失败,这就是外碎片问题

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7Wy4Fv3W-1676831404624)(G:\Typora\图片保存\image-20230217233758799.png)]

    除了外碎片还会有内碎片问题,一般是是由于一些对齐的需求,导致分配出去的空间中一些内存无法被利用;例如struct字节对齐数

    2.申请效率的问题:

    ​ 每次申请都会占用CPU的时间,申请地越多越频繁,消耗就越大,效率就越低

    例如上学家里给生活费,假设一学期的生活费是6000块。

    方式1:开学时6000块直接给你,自己保管,自己分配如何花。

    方式2:每次要花钱时,联系父母,父母转钱。

    ​ 同样是6000块钱,第一种方式的效率肯定更高,因为第二种方式跟父母的沟通交互成本太高了。 同样的道理,程序就像是上学的童鞋,操作系统就像父母,频繁申请内存的场景下,每次需要内存,都向系统申请效率必然有影响。

    而内存池可以减少频繁调用malloc而消耗操作系统,尽可能的降低内存碎片问题。


定长内存池

​ 在介绍高并发内存池之前,先用定长内存池来对内存池有一个初步的认识,并且可用于后续在高并发内存池中的一个组件

​ 在 C、C++中申请内存使用的都是malloc,什么场景下都可以使用,但也就表示在任何场景下的性能都是中庸的;那么可以针对特定的场景设计一个定长的内存池

例如,设计一个分配32字节对象的固定内存分配器来对比malloc(32)

定长内存池设计

如图,下面将围绕图中这三部分来设计,具体细节再代码中体现

申请内存池

这里没有什么难度,就是第一次申请内存池时,先判断记录内存池的指针为不为空,为空就表示这是第一次

	T* New()
	{
		if (_memory == nullptr)
		{
			_memory = (char*)malloc(128 * 1024);
			if (_memory == nullptr)
			{
				throw bad_alloc();
			}
		}
	}
//成员变量
private:
	char* _memory = nullptr;	//记录内存块位置的指针
	void* _freeList = nullptr;	//链接释放后的内存块的,单链表头指针

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bUsrco5l-1676831404625)(G:\Typora\图片保存\image-20230206184739305.png)]

选用char*做为指针的类型,因为是char是以1字节为单位的,可以切割任意大小的字节,用户申请时只需要加上强转即可

用户申请

创建好内存池后,用户就可以去内存池中申请了,每申请一块,_memory就往后移一块

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hpf5xDVZ-1676831404626)(G:\Typora\图片保存\image-20230206190258680.png)]

T* New()
{
    if (_freeList == nullptr)
    {
        _memory = (char*)malloc(128 * 1024);
        if (_memory == nullptr)
        {
            throw bad_alloc();
        }
    }
    
    T* obj = _memory;		//用户申请
    _memory += sizeof(T);	//申请后更新指针位置
    
	new(obj)T;				//定位new显式调用T类型构造函数,给用户初始化
	return obj;
}

​ 当此内存池申请满了后,再有用户申请时,_memory就不能往后移了,需要再去向系统申请定长的内存。

​

​ 但是申请内存池处的判断依据是_memory为空,内存池满了后_memory的位置不为空,因为这只是我们申请下来的定长内存,后面还有内存,只是不属于我们。

​ 所以这里的解决办法是增加一个成员变量,用于记录剩余字节数量,之后再更改申请内存池的判断依据

	T* New()
	{
		if (_remainedBytes < sizeof(T))		//假如剩余的内存比要申请的小,则也需重新申请,剩余的就丢弃
		{									//但一般再申请时会根据要申请的定长,设置出能整除的大小
			_remainedBytes = 128 * 1024;	
			_memory = (char*)malloc(128 * 1024);
			if (_memory == nullptr)
			{
				throw bad_alloc();
			}
		}

		T* obj = (T*)_memory;
		_memory += sizeof(T);
		_remainedBytes -= sizeof(T);
        new(obj)T;				//定位new显式调用T类型构造函数,给用户初始化
		return obj;
	}
private:
	char* _memory = nullptr;		//记录内存块位置的指针
	void* _freeList = nullptr;		//链接用户释放后的内存块 链表的头指针
	size_t _remainedBytes = 0;		//记录剩余字节数 

用户归还(释放)

这里的策略是,将用户释放后的内存块,用链表链接采用头插策略,每个内存块的前4或8个字节用来记录下一个内存块的地址

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EVJ9u8Gp-1676831404626)(G:\Typora\图片保存\image-20230206194410025.png)]

获取前4/8字节的方法如下:

T* obj//用户释放的内存块
*(void**)obj = _freeList
_freeList = obj;

//	指针的大小是根据32、64位来定的,使用指针就可以自动根据系统位数使用了
//	但假如直接强转为一级指针记录地址,等于是更改指向内存块的首地址;所以用二级指针,解引用后才是更改内容,让下一个内存块地址做为内存块的内容

用户释放内存代码如下:

void Delete(T* obj)
{
    *(void**)obj = _freeList;		//先让obj指向的内存块的内容为下一块的地址
    _freeList = obj;				//再让更新头指针
}

同时要注意:

  • 因为申请的是定长大小,所以回收用户释放的内存块是可以再次使用的;
  • 这里使用的是模板,T可以代表任何类型大小,包括int、char等等;这些不够4/8字节的,只能在申请时补齐到8字节

综上,对申请内存的代码改动如下:

	T* New()
	{
        T* obj = nullptr;
        if (_freeList)							//查看释放回来的内存块队列是否为NULL
        {
            obj = (T*)_freeList;
            void* next = *(void**)_freeList;
            _freeList = next;
        }
        else
        {
            if (_remainedBytes < sizeof(T))		//假如剩余的内存比要申请的小,则也需重新申请,剩余的就丢弃
            {									//但一般再申请时会根据要申请的定长,设置出能整除的大小
                _remainedBytes = 128 * 1024;	
                _memory = (char*)malloc(128 * 1024);
                if (_memory == nullptr)
                {
                    throw bad_alloc();
                }
                
                obj = (T*)_memory;
                //如果对象的字节大小小于指针的字节大小,就给他指针大小的内存块,否则就该多大给多大
                size_t objSize = sizeof(T) < sizeof(T*) ? sizeof(T*):sizeof(T);//所以根据这里在使用时最好算算能不能整除
                _memory += objSize;
                _remainedBytes -= objSize;
            }           
        }
        new(obj)T;				//定位new显式调用T类型构造函数,给用户初始化
		return obj;
	}

总体代码

#include <iostream>
#include <vector>
#include <time.h>

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

template<class T>
class ObjectPool
{
public:
	T* New()
	{
		T* obj = nullptr;
		if (_freeList)							//查看释放回来的内存块队列是否为NULL
		{
			obj = (T*)_freeList;
			void* next = *(void**)_freeList;
			_freeList = next;
		}
		else
		{
			if (_remainedBytes < sizeof(T))		//假如剩余的内存比要申请的小,则也需重新申请,剩余的就丢弃
			{									//但一般再申请时会根据要申请的定长,设置出能整除的大小
				_remainedBytes = 128 * 1024;
				_memory = (char*)malloc(128 * 1024);
				if (_memory == nullptr)
				{
					throw bad_alloc();
				}
			}            
            obj = (T*)_memory;
            //如果对象的字节大小小于指针的字节大小,就给他指针大小的内存块,否则就该多大给多大
            size_t objSize = sizeof(T) < sizeof(T*) ? sizeof(T*) : sizeof(T);//所以根据这里在使用时最好算算能不能整除
            _memory += objSize;
            _remainedBytes -= objSize;
        
		}
        new(obj)T;				//定位new显式调用T类型构造函数,给用户初始化
        return obj;
	}


	void Delete(T* obj)
	{
		*(void**)obj = _freeList;		//先让obj指向的内存块的内容为下一块的地址
		_freeList = obj;				//再让更新头指针
	}


private:
	char* _memory = nullptr;
	void* _freeList = nullptr;
	size_t _remainedBytes = 0;
};

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nAnJ2DpJ-1676831404627)(G:\Typora\图片保存\image-20230206211924254.png)]

测试对比(malloc)

下面使用malloc与上述定长内存池进行测试对比

struct TreeNode
{
	int _val;
	TreeNode* _left;
	TreeNode* _right;

	TreeNode()
		:_val(0)
		, _left(nullptr)
		, _right(nullptr)
	{}
};

void TestObjectPool()
{
	// 申请释放的轮次
	const size_t Rounds = 5;

	// 每轮申请释放多少次
	const size_t N = 100000;

	std::vector<TreeNode*> v1;
	v1.reserve(N);

	size_t begin1 = clock();
	for (size_t j = 0; j < Rounds; ++j)
	{
		for (int i = 0; i < N; ++i)
		{
			v1.push_back(new TreeNode);
		}
		for (int i = 0; i < N; ++i)
		{
			delete v1[i];
		}
		v1.clear();
	}

	size_t end1 = clock();

	std::vector<TreeNode*> v2;
	v2.reserve(N);

	ObjectPool<TreeNode> TNPool;
	size_t begin2 = clock();
	for (size_t j = 0; j < Rounds; ++j)
	{
		for (int i = 0; i < N; ++i)
		{
			v2.push_back(TNPool.New());
		}
		for (int i = 0; i < N; ++i)
		{
			TNPool.Delete(v2[i]);
		}
		v2.clear();
	}
	size_t end2 = clock();

	cout << "new cost time:" << end1 - begin1 << endl;
	cout << "object pool cost time:" << end2 - begin2 << endl;
}
/********************************************************************************/
#include "ObjectPool.h"
int main()
{
	TestObjectPool();

	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cRXuE6eO-1676831404627)(G:\Typora\图片保存\image-20230206210030968.png)]

内存池是不释放的,因为用户归还的内存块是不连续的,只要进程正常退出,系统是自动回收的

(也可以在申请内存池时记录每个内存池的首地址)

  • 拓展(绕过malloc,直接系统调用)
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 ptr;
}


//申请内存的 T* new()
if (_remainBytes < sizeof(T))
{
    _remainBytes = 128 * 1024;
    //_memory = (char*)malloc(_remainBytes);
    _memory = (char*)SystemAlloc(_remainBytes >> 13);
    if (_memory == nullptr)
    {
        throw std::bad_alloc();
    }
}

高并发内存池框架简介

concurrent memory poll整体分为三个模块

  1. thread cache:

    ​ 这部分是给每个线程都配有一个cache,用于小于256KB的内存(申请、释放),都直接从这里分配,并且因为是每个线程都有一个cache,所以这里不需要锁,这也是并发内存池高效的地方

  2. central cache:

    ​ 本层是所有线程都会共享的中心缓存,当Thread Cache中的缓存不能满足用户的需求,会按需向本层申请,本层根据thread Cache的需求分配给它想要大小的内存块,同时还会周期性的回收内存块,避免个别线程占用内存过多达到均衡分配按需调度的目的。

    ​ 但central cache是存在锁竞争问题的,因为是多个线程共享central cache;这里采用(哈希桶)桶锁,并且是在thread cache没内存时才会找central cache,所以锁的竞争不会很紧张。

  3. page cache:

    ​ 本层是页缓存,分配是按页为单位的,负责给第二层提供内存,并会根据第二层的内存使用情况进行回收再与其他页合并,之后再分配时又会以整页为单位分配给第二层,可以减少内存碎片问题。

这三层底层的数据结构都会用到哈希桶与链表,具体细节在后面分别介绍


thread cache的设计

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u2N6bo23-1676831404628)(G:\Typora\图片保存\image-20230208134547025.png)]

​ thread cache是哈希桶结构,每个桶是一个根据桶位置映射的挂接内存块的自由链表,每个线程都会有一个thread cache对象,这样就可以保证线程在申请和释放对象时是无锁访问的


申请部分

thread cache的申请流程:

  • 刚开始启动时的哈希桶没有挂载任何内存的,当用户第一次申请内存时—>
  • 会先向第二人层申请,第二层会给第一层想要大小的内存块—>
  • 但是一开始是给一块,后面是慢慢增加的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NuSV3Y0o-1676831404628)(G:\Typora\图片保存\image-20230213031817974.png)]

​ thread cache的底层是哈希桶结构,内存块是以自由链表的形式挂载的(桶),并且thread cache层最大可处理内存为256KB,也就是说256kb映射最大的桶

thread cache内存的映射与分配:

  • 因为是自由链表的形式,所以需要用头部的4或8字节(这里直接定义为8字节)去记录下一块内存的地址,所以最小为8字节的桶

    static void*& NextObj(void* obj)
    {
    	return *(void**)obj;
    }
    
  • 但是从8字节往后例如9、10、11字节一直到256K字节的数字非常大,会划分出大量的哈希桶,还可能会导致较大的内存开销,所以不可能每个数都映射一个桶,具体分配规则如下:

    	 整体控制在最多10%左右的内碎片浪费
    	 [1,128]						8byte对齐				freelist[0,16)		------16个桶
    	 [128+1,1024]					16byte对齐			freelist[16,72)		------56个桶
    	 [1024+1,8*1024]				128byte对齐			freelist[72,128)	------56个桶
    	 [8*1024+1,64*1024]			1024byte对齐			freelist[128,184)	------56个桶
    	 [64*1024+1,256*1024]			8*1024byte对齐		freelist[184,208)	------24个桶
    
    	例如129对齐是144,浪费15个字节,15/144 = 0.104...1-56浪费率会高,但是前几个对齐的字节数都为8,最大浪费也就7字节,可以忽略
    

    例如 [1,128]有16个桶:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GMVnUpAL-1676831404628)(G:\Typora\图片保存\image-20230215221140191.png)]

所以这里不仅要根据用户的申请的字节数计算对齐数,还要计算映射的桶号

  • 对象的框架搭建

    /**************************************Common.h*************************************/
    /******************************************自由链表(桶)******************************************/
    static const size_t MAX_BYTES = 256 * 1024;	//最大字节数
    static const size_t NFREELIST = 208;		//桶的数量
    class FreeList
    {
    public:
        //头删,用于用户申请
    	void* Pop()
    	{
    		assert(_freeList);
    		void* obj = _freeList;
    		_freeList = NextObj(_freeList);
    		_size -= 1;
    		return obj;
    	}
    	//判断是否为NULL
    	bool Empty()
    	{
    		return _freeList == nullptr;
    	}
    	//用于向上申请需要的内存块数,是递增的,具体用法在后面代码中介绍
    	size_t& MaxSize()
    	{
    		return _maxSize;
    	}
    	//批量头插,用于向第二层申请批量内存块
    	void PushRange(void* start,void* end,size_t n)
    	{
    		NextObj(end) = _freeList;
    		_freeList = start;
    		_size += n;
    	}
    
    	//获取内存块个数
    	size_t Size()
    	{
    		return _size;
    	}
    private:
    	void* _freeList = nullptr;	//头指针
    	size_t _maxSize = 1;		//用于慢启动,具体用法在申请函数中介绍
    	size_t _size = 0; 			//记录每个链表上的内存块个数
    };
    
    /******************************************2.thread cache.h****************************************/
    #pragma once
    #include "Common.h"
    
    class ThreadCache
    {
    public:
    	//申请
    	void* Allocate(size_t size);
    
    	//从第二层,中心缓存获取
    	void* FetchFromCentralCache(size_t index, size_t size);
    
       
    private:
    	FreeList _freeList[NFREELIST];//数组长208,也就是有208个桶,即挂载208个自由链表
    };
    
    static _declspec(thread) ThreadCache* pTLS_threadCache = nullptr;
    /*每个线程各自持有一个
    线程局部存储(TLS),是一种变量存储的方法,这个变量在它所在线程内是全局可访问的,但不能被其他线程访问到
    */
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LwZXUnpx-1676831404629)(G:\Typora\图片保存\image-20230215145916850.png)]

  • 字节对齐数与桶号的映射算法

    class SizeClass
    {
    
    	//计算对齐数,例如9字节,对齐数就是16
    	/*方法1:
    	inline static size_t _RoundUp(size_t size,size_t alignNum)
    	{
    		size_t alignSize;
    		if (size % alignNum != 0)
    		{
    			alignSize = (size / alignNum + 1) * alignNum;// 9 % 8=1 + 1 =2 * 8 =16
    		}
    		else
    		{
    			alignSize = alignNum;
    		}
    		return alignSize;
    	}*/
    	//方法2:
    	inline static size_t _RoundUp(size_t bytes, size_t alignNum)
    	{
    		return ((bytes + alignNum - 1) & ~(alignNum - 1));
    	}
    
    
    	//计算哈希桶号,也就是哈希映射数组的下标
    	/*方法1:
    	size_t _Index(size_t bytes, size_t alignNum)
    	{
    		if (bytes % alignNum == 0)
    		{
    			return bytes / alignNum - 1;
    		}
    		else
    		{
    			return bytes / alignNum;
    		}
    	}*/
    	//方法2:
    	static inline size_t _Index(size_t bytes, size_t align_shift)	
    	{
    		return ((bytes + (1 << align_shift) - 1) >> align_shift) - 1; 
    	}																  
    	//9,3
    	//9 + (1 << 3) - 1 = 8 - 1 + 9 = 16
        //16 >> 3 - 1 = 1
    
    public:
    	//计算thred_cache用户申请字节的对齐数 
    	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
    		{
    			//return _RoundUp(size, 1 << PAGE_SHIFT);		//超过256KB处理方式,具体处理方式在后面介绍
                return -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)
    		{																		//bytes - 128:当前区间的字节数 - 上一区间的总字节数
    			return _Index(bytes - 128, 4) + group_array[0];						//group_array[0]:1~128区间的总桶数量
    		}
    		else if (bytes <= 8 * 1024)
    		{
    			return _Index(bytes - 1024, 7) + group_array[1] + group_array[0];	//group_array[1] + group_array[0] = 1~1024间的总桶数
    		}
    		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;
    		}	
    	}
    
    
    	// 每一次thread cache要从中心缓存获取多少个内存块
    	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;
    	}
    
    private:
    
    };
    
  • 具体执行部分

    //用户申请
    void* ThreadCache::Allocate(size_t size)
    {
    	assert(size <= MAX_BYTES);
    	//1.计算用户申请的字节对应的对齐数
    	size_t allignSize = SizeClass::RoundUp(size);
    	//2.计算用户要到哪个桶取内存块(数组映射的下标中挂载的链表取内存块)
    	size_t index = SizeClass::Index(size);
    
    	//判断index下标的FreeList中的头指针是否为NULL
    	if (!_freeList[index].Empty())
    	{
    		return _freeList[index].Pop();					//不为NULL就可以Pop给用户了
    	}
    	else
    	{
    		return FetchFromCentralCache(index,allignSize);//为NULL就需要向中心缓存申请
    	}
    }
    
    
    
    
    //当用户申请时thread没有,则向第二层申请
    void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
    {
        //thread申请只会每次申请1个,但是central可以每次多给一些,就不用频繁申请了,所以这里采用慢启动调节分配个数
        
    	//1.慢启动调节
    	//(1)最开始不会一次向central 批给cache太多,可能会用不完
    	//(2)会从1个内存块开始递增给它(batchNum),每申请一次下次就会多一个,直到与算法算的数值持平,再改用算法给出的数值
    	//(3)相关算法给出的数值:要的字节数越大,给cache的内存块就越少,要的字节数越小,给cache批的内存块就越多
    	size_t batchNum = min(_freeList[index].MaxSize(), SizeClass::NumMoveSize(size));	
    	if (batchNum == _freeList[index].MaxSize())
    	{
    		_freeList[index].MaxSize() += 1;
    	}
        
    	//2.向第二层申请
        //(1)参数设置:输出型参数,去第二层获取内存块组成链表的头和尾的地址
    	void* start = nullptr;
    	void* end = nullptr;
    	//(2)向第二层的哈希桶内获取批量内存块(参数:头部位置,结尾位置,申请的个数,申请的字节大小)(第二层函数接口)
    	size_t actualNum = CentralCache::GetInstance()->FetchRangeObj(start, end, batchNum, size);
    	//(3)根据返回值判断获取的个数,给的返回值(actualNum)不一定要多少就能拿到多少,例如要4个但只剩2个,就只能返还2个
    	assert(actualNum > 0);
    	
    	if (actualNum == 1)					//假如就剩1个,直接给用户
    	{
    		assert(start == end);
    		return start;
    	}
    	else								//超过一个则留下头一个,其他的批量头插到thread的哈希桶的自由链表里
    	{
    		_freeList[index].PushRange(NextObj(start), end, actualNum - 1);
    		return start;
    	}
    }
    

    示例,如图下图所示:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZpbsQa9Z-1676831404629)(G:\Typora\图片保存\image-20230215153604579.png)]

  • 当用户申请了10字节的内存会发生什么?

    (1)会先经过相关算法算出字节对齐数为16字节,并且算出桶号为1号桶

    (2)会去1号桶拿内存,1号桶没有内存,则向第二层central cache申请,申请的个数会根据慢启动的算法算出本次向上申请多少个

    (3)第二层返回第一层的个数只有一个则直发给用户,若返回多个,则把头一个发给用户,剩下的直接批量挂载到1号桶上

    注意:向第二层申请的个数,不要一定全都能拿到,这里的申请只是期望申请这么多,但最少必须返回一个给用户

申请部分需要注意的是:

  • 对齐数的算法与映射的算法必须要准确
  • 拿到第二层给的内存块自由链表后,更新头指针的指向与末尾内存块的指向

释放部分

thread cache的释放流程:

  • 用户释放后先算出释放的字节数对应的桶号,之后直接头插到对应的桶上
  • 之后会检查该桶上挂载的内存个数
  • 假如这个字节数的桶挂载的个数,大于向第二层批量申请的个数,则批量返回一部分,返回的个数就是批量申请的个数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WpxAUQpD-1676831404636)(G:\Typora\图片保存\image-20230215162756819.png)]

  • 释放部分在class FreeList的框架中需要添加两个函数
void Push(void* obj)
{
    assert(obj);
    //头插,用户释放后插入到thread cache对应的桶中
    NextObj(obj) = _freeList;
    _freeList = obj;
    _size += 1;

}	

//批量头删,用于还给第二层 central cache
void PopRange(void*& start,void*& end,size_t n)
{
    start = _freeList;
    end = start;
    for (size_t i = 0; i < n - 1; i++)
        end = NextObj(end);

    _freeList = NextObj(end);
    NextObj(end) = nullptr;	
    _size -= n;

}
  • 具体执行部分,在class ThreadCache增加以下两个函数的声明
//用户释放
void ThreadCache::Deallocate(void* ptr, size_t size)
{
	assert(ptr);
	assert(size <= MAX_BYTES);
	//1.计算释放回来的字节数对应的桶,并头插
	size_t index = SizeClass::Index(size);
	_freeList[index].Push(ptr);
	//2.判断该桶挂载的内存块个数,是否达到归还第二层的个数标准
	if (_freeList[index].Size() > _freeList[index].MaxSize())
	{
		ListTooLong(_freeList[index], size);
	}
}
//释放给第二层
void ThreadCache::ListTooLong(FreeList& list, size_t size)
{
    //1.参数设置:输出型参数
	void* start = nullptr;
	void* end = nullptr;
	//2.批量头删:取MaxSize()个,并拿到取到的 整个自由链表的 头地址和尾地址
	list.PopRange(start, end, list.MaxSize());
	//3.释放给第二层 central cache(第二层的函数接口)
	CentralCache::GetInstance()->ReleaseListToSpans(start, size);
}

释放部分需要注意的是,批量删除时:

  • 设置好末尾内存块下一个指向为NULL
  • 更新头指针的指向

第一层的测试

  • 首先需要提供给用户的接口
/*********************ConcurrentAllocl.h*********************/
#pragma once

#include "Common.h"			//存放FreeList框架的头文件 
#include "ThreadCache.h"	//存放ThreadCache的头文件

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

  • 将涉及到第二层的接口暂时写个空客将返回值设置为nullptr
void Alloc1()
{
	//线程1,获取5次6字节
	for (size_t i = 0; i < 5; i++)
	{
		void* ptr = ConcurrentAlloc(6);
	}
}
void Alloc2()
{
	//线程2,获取5次7字节
	for (size_t i = 0; i < 5; i++)
	{
		void* ptr = ConcurrentAlloc(7);
	}
}
void TLSTest()
{
	std::thread t1(Alloc1);
	t1.join();

	cout << endl;

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

int main()
{
	TLSTest();
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D2huajrH-1676831404637)(G:\Typora\图片保存\image-20230216004441046.png)]

如图所示:所有线程都调用同一个申请函数ConcurrentAlloc(),并且没有加锁,而且都申请了一个thread cache对象

所以这部分可以分解决争锁问题(释放暂时无法测试)


central cache的设计

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O4aLexrC-1676831404637)(G:\Typora\图片保存\image-20230215172749139.png)]

​ 这层的整体结构与第一层类似,也是哈希桶,并且映射规则和第一层是一样的,不过这层的桶的类型不是FreeList,而是SpanList,是个双链表,链表上每个结构是一个Span对象,Span中挂载的是内存块(单链表)

​ 虽然这里是共享资源,但是可以采用桶锁,也就是只锁具体的一个桶,可以减少锁的竞争

申请部分

central cache的申请流程:

thread向central申请X个内存块,会先根据申请字节数算法算出桶号,之后会在这个桶里找一个非空的Span:

(1)找到一个非空Span,有足够的内存块,可以给thread

(2)找到一个非空Span,不够X个,会把剩余的全给用户(至少有一个够给用户的)

(3)没有不空的Span

  • 前两种情况,Span会数够x个给thread层或者不够x个把剩余的给thread层

  • 第三种情况:

    • central会向Page层申请一个Span,此时的Span至少带有一页大小的内存,页的首地址会以特殊的方式存在Span中

    • central拿到这个Span后会对整个页进行切成若干个内存块,内存块的大小是thread申请字节数的大小

    • 最后在分给thread想要的个数

  • 记录整页的地址一般是除以 一个页大小的字节数

    为了效率起见,将线性地址空间分成若干大小相等的片,称为页( Page )。常见的页面大小为 4KB,每一页都有 4K 字节长,每一页的起始地址都能被 4K 整除。

    我的开发环境一页大小是8k,一般系统在分配一页内存时,每页的起始地址都可以被8K整除,并且在这个页内的所有的地址除8K取整都是同一个数;

    这样在回收内存块时,就可以找到对应的Span了(具体释放细节在后面介绍,这里只先了解记法)

  • central cache的哈希桶与thread cache用的是同一套映射规则

  • 我的开发环境一页是8K,而central cache在向上层申请Span时,当申请很大的内存时例如256KB,那么一页肯定不够用。所以在申请时,需要计算出申请几页的内存

    // 计算一次向系统获取几个页
    //参数是由用户要申请的字节数传给thread后计算的对齐数,不清楚可以翻看thread申请的函数FetchFromCentralCache
    static const size_t PAGE_SHIFT = 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;
    }
    //会返回计算出要申请页的个数
    
  • 对象的框架搭建

    /*********************Common.h*********************/
    
    //根据系统位数不同来调整页号的数据类型
    #ifdef _WIN64
    typedef unsigned long long PAGE_ID;
    #elif _WIN32
    typedef size_t PAGE_ID;
    #endif
    
    // 管理多个连续页大块内存跨度的结构(即管理一页或多个页的对象)
    struct Span
    {
    	PAGE_ID _pageId = 0;		// 大块内存起始页的页号,也就是页的首地址除以8K后的数字
    	size_t  _n = 0;				// 页的数量
    
    	Span* _next = nullptr;		// 双向链表的结构——>尾指针
    	Span* _prev = nullptr;		// 双向链表的结构——>头指针
    
    	size_t _useCount = 0;		// 切好小块内存,被分配给thread cache的计数,每分走一个就+1
    	void* _freeList = nullptr;  // 切好的小块内存的自由链表
    };
    
    
    class SpanList
    {
    public:
    	SpanList()
    	{
    		_head = new Span;
    		_head->_prev = _head;
    		_head->_next = _head;
    	}
    	//指定位置的头插一个Span
    	void Insert(Span* pos, Span* newSpan)
    	{
    		assert(pos && newSpan);
    
    		Span* newSpan_prev = pos->_prev;
    
    		newSpan->_next = pos;
    		newSpan->_prev = newSpan_prev;
    		newSpan_prev->_next = newSpan;
    		pos->_prev = newSpan;
    	}
    	//指定位置的头删一个Spaan
    	void Earse(Span* pos)
    	{
    		assert(pos);
    		assert(pos != _head);
    
    		Span* pos_next = pos->_next;
    		Span* pos_prev = pos->_prev;
    
    		pos_next->_prev = pos_prev;
    		pos_prev->_next = pos_next;
    	}
    	//SpanList的第一个Span
    	Span* Begin()
    	{
    		return _head->_next;
    	}
        //SpanList的最后一个Span
    	Span* End()
    	{
    		return _head;
    	}
        //判断SpanList是否为 NULL
    	bool Empty()
    	{
    		return _head == _head->_next;
    	}
    	//头插
    	void PushFront(Span* span)
    	{
    		Insert(Begin(),span);
    	}
    	//头删
    	Span* PopFront()
    	{
    		Span* Front = _head->_next;
    		Earse(Front);
    		return Front;
    	}
    
    	std::mutex _mtx; // 桶锁
    private:
    	Span* _head;	//头指针
    
    };
    
    
  • central cache框架搭建

    central cache是所有线程共享的,所以只能有一个,这里就要用到单例模式

    /*********************CentralCache.h*********************/
    //单例模式——饿汉模式
    class CentralCache
    {
    	CentralCache(){}
    	CentralCache(const CentralCache& cc) = delete;
    	CentralCache& operator=(CentralCache cc) = delete;
    public:
    	//提供获取_sInst的唯一接口
    	static CentralCache* GetInstance()
    	{
    		return &_sInst;
    	}
    	
    	//期望批量从span获取内存块给thread
    	size_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size);
    	//获取一个非空的Span
    	Span* GetOneSpan(SpanList& list,size_t byte_size);
    	// 将一定数量的对象释放到span跨度
    	void ReleaseListToSpans(void* start, size_t byte_size);
    private:
    	SpanList _spanList[NFREELIST];
    	static CentralCache _sInst;		//初始化放在 CentralCache.cpp中,程序运行即初始化
    };
    
    

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F4eLmVJY-1676831404637)(G:\Typora\图片保存\image-20230215235918853.png)]

​ central属于临界资源,需要加锁,但这里加锁加的是桶锁:不同的线程访问不同的桶,就对不同的桶加锁,这样可以减少锁的竞争,只有多个线程在同一个桶申请资源时才会争锁

  • 具体执行部分

    CentralCache CentralCache::_sInst;
    //从central获取内存块给thread
    //这里参数中的start end就是thread中调用FetchFromCentralCache函数时的start end
    size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size)
    {
    	//1.算一下在central中的哈希桶号(数组下标)
    	size_t index = SizeClass::Index(size);
    	
        //2.指定index号桶 加锁
    	_spanList[index]._mtx.lock();
    	
        //3.获取index号桶中的非空span
    	Span* span = GetOneSpan(_spanList[index],size);
    	assert(span);
    	assert(span->_freeList);
    
    	//4.遍历这个span,数出需要多少个内存块
    	start = span->_freeList;		
    	end = start;
    	//start此时指向的就是第一个,actualNum = 1
    	size_t actualNum = 1;
    	//span中的内存块也可能达不到预期的个数,所以碰到nullptr也要终止遍历
    	while (actualNum < batchNum && NextObj(end) != nullptr)
    	{
    		end = NextObj(end);	
    		actualNum++;	
    	}
        
    	//5.更新相关的属性
        
        //(1)更新头指针的指向
        //例如要2个,那么end的位置正好就在第二个位置,所以头指针指向的应该是end的下一个
    	span->_freeList = NextObj(end);	
    	//(2)更新end的next指向
    	NextObj(end) = nullptr;			
    	//(3)更新拿走多少个小块内存
    	span->_useCount += actualNum;
    	//到这里,这个span已经删掉了actualNum个,采用的是在指定位置尾删
        
        //6.解除index号桶的锁
    	_spanList[index]._mtx.unlock();
    	//返回给thread的个数
    	return actualNum;
    }
    
    
    
    //获取一个非空的Span
    Span* CentralCache::GetOneSpan(SpanList& list, size_t byte_size)
    {
    	//1.先找到非空的Span
        //(1)从第一个开始判断,中途有非空的直接返回Span的地址,没有则继续往下找,直到全都没有
    	Span* it = list.Begin();
    	while (it != list.End())
    	{
    		if (it->_freeList != nullptr)
    			return it;
    		else
    			it = it->_next;
    	}
        //(2)全都没有则向page申请,此时本线程转到page中,但是其他线程可能会释放需要进入这个桶,所以这里先解锁
    	list._mtx.unlock();
        
    	//2.循环结束没有找到,则向Page索要
        //(1)page也属于共享资源所以也要加它的锁,page部分具体在后面介绍
    	PageCache::GetInstance()->_mtx.lock();
        //(2)先根据这个字节数计算需要多少页,之后再申请
    	Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(byte_size));
        //(3)解page的锁
    	PageCache::GetInstance()->_mtx.unlock();
    	
        //3.拿到Page给的Span,开始按申请的字节数(byte_size)切分
        //(1)将此Span记录的页号乘8K,恢复到16位首地址的表示方式,之后强转为char*,方便切分
    	char* start = (char*)(span->_pageId << PAGE_SHIFT);
        //(2)将页的个数乘8k,计算一共有多少个8k,也就是所有页加一起的总字节数
    	size_t bytes = span->_n << PAGE_SHIFT;
    	//(3)记录这个Span携带所有页总和的末尾地址
    	char* end = start + bytes;
        //(4)开始切分,先更新头指针指向
    	span->_freeList = start;
    	start += byte_size;
        //(5)开始遍历,start经过byte_size个字节跳转到下一位置,tail的next指向start的位置,完成一次切分
    	void* tail = span->_freeList;
        //采用的是尾插法,这样可以让内存尽量连续,提高缓存利用率
    	while (start < end)	
    	{
    		NextObj(tail) = start;
    		tail = start;//tail = NextObj(tail);	
    		start += byte_size;
    	}
        //(6)切分完成后,设置末尾的内存块的next指向NULL
    	NextObj(tail) = nullptr;
        
        //4.向page申请内存的部分结束,重新去争锁,因为要访问桶,把刚获得的Span挂载到central的桶上
    	list._mtx.lock();
        //5.把刚切完的内存块挂载到这个Span的头指针上
    	list.PushFront(span);
    	//6.返回刚获得并切完的Span
    	return span;
    }
    
    

    当thread向central获取一些24B大小的内存块,central先去2号桶找一个非空的Span,没找到后向Page申请,申请流程示例如下:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rZS9jruE-1676831404637)(G:\Typora\图片保存\image-20230216023215331.png)]

申请部分需要注意的是:

  • central是所有thread共享的资源,需要加锁,但是加的是桶锁,但是在向page申请Span并切分时可以暂时解锁,之后在加上
  • central是所有thread共享的,只能有一个,所以需要使用单例模式
  • 在处理内存块链表时,容易遗漏头指针、尾部指向等的更新

释放部分

central cache释放流程:

  • thread中某一个桶所持有的内存块个数超过了批量申请的个数后,会以批量申请的数字 为释放数字 ,批量释放内存块给central
  • central拿到批量的内存块后会先找对应的桶,找到对应的桶后再找对应的Span挂载到对应Span上

  • 并让 useCount 减去相应的数量,当_useCount减为0时,将所这个Span释放回Page

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5fxkRVbA-1676831404638)(G:\Typora\图片保存\image-20230216041348716.png)]

  • 根据返回的内存块查找对应的Span

    ​ 内存号是页的首地址整除8K得到的,这一点在介绍central cache申请部分提到过:每一页的起始地址都能被8K整除,并且这一页内任意一个地址除8K取整的数字都是同一个数字(页号)

    所以只要Span记录了页号和也的个数,碎内存块就能找到对应的Span,而Page在分给central一个Span之前不仅让它记住页号,还建立一个记录页号与Span的映射关系。这样就可以把central拿到的批量内存块,挂载到对应Span上了

  • 不用关心内存碎块的顺序问题

​ 只要这个Span内所有的内存都返回了(useCount=0),就证明不会有任何人正在使用Span内的内存,并且已经是用户释放了的

​ 假如经Page回收,后面需求大了在向page申请时,又申请到这个页的内存,直接从首地址按地址顺序切分就行了,所以不用管内存块是否需要按序排序;

所以Span内,页号(首地址)、页的个数、页的大小等这些属性信息是很重要的

  • 代码部分
void CentralCache::ReleaseListToSpans(void* start, size_t byte_size)
{
	//1.算桶号
	size_t index = SizeClass::Index(byte_size);
	//2.加锁,只要访问到共享资源就需要加锁,而释放就是在Span中插入内存块,所以是访问共享资源,需要加锁
	_spanList[index]._mtx.lock();
	//3.因为返回的批量内存块,不可能都属于同一个Span,所以对每个内存块都查找一次
	while (start)
	{
        //(1)记录next的位置,方便后续查找
		void* next = NextObj(start);
        //(2)对每个内存块,都查找对应的Span,此函数接口是page中的(根据地址获取对应的Span)
		Span* span = PageCache::GetInstance()->MapObjectToSpan(start);
		//(3)找到对应的Span后,头插
		NextObj(start) = span->_freeList;
		span->_freeList = start;
        //(4)更新这个Span记录分走内存块的个数
		span->_useCount--;
		//(5)当一个span所有的内存块都回来后,将这个Span释放给Page
		if (span->_useCount == 0)
		{
			_spanList[index].Earse(span);	//删除这个Span
			span->_next = nullptr;			//清除前后指针记录的位置,防止发生越界访问等错误
			span->_prev = nullptr;			//......
			span->_freeList = nullptr;		//既然这个页的内存块都已回收,就可以不用头指针记录了,有页号就够了

            //前面都已经清除这个Span在桶中的关联了,再向page释放Span时就可以暂时解锁了
			_spanList[index]._mtx.unlock();	
            //page也是只有一个也是共享的,central在向page释放page时,是在访问page这个共享资源,所以需要加page相关的锁
			PageCache::GetInstance()->_mtx.lock();
            //page的函数接口,功能是把centraal释放的span插入到page相关的结构中
			PageCache::GetInstance()->ReleaseSpanToPageCache(span);
            //解page的锁,争central的桶锁
			PageCache::GetInstance()->_mtx.unlock();
			_spanList[index]._mtx.lock();
		}
        //4.当前内存块找完,准备找下一个
		start = next;
	}
	//解锁
	_spanList[index]._mtx.unlock();
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k5z8YDcF-1676831404638)(G:\Typora\图片保存\image-20230216200005414.png)]

释放部分需要注意的就是对锁的操控,其次就是不要遗漏更新Span的相关信息


Page Cache的设计

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z3JJYDbJ-1676831404638)(G:\Typora\图片保存\image-20230216201148736.png)]

​ Page的哈希桶和前两层不一样,它只有128个桶,挂载的都是Span,桶号表示Span所携带的页数;每个Span都是一样大的对象,但每个Span中记录的属性是不一样的,例如SpanA中的页数为2,共16K内存,SpanB中的页数为1,共8K内存。

​ 所以central申请的是一个Span,Span会把它携带的页切成central要的小块挂载在自己的链表上

申请部分

Page Cache的申请流程:

  • 假如central向page申请2页的Span

  • page根据申请的页数,先去[1]号桶找Span可能会出现以下3种情况:

    (1)[1]号桶中有Span可用,则记录页号与Span的映射后,将Span发送给central

    (2)[1]号桶为NULL,则往后继续找不为NULL的桶,例如[2]号桶不为NULL,则把[2]号桶头一个Span切成,1页的Span和2页的Span,之后记录好他们的页号与Span的映射关系,1页的Span挂到[0]号桶上,2页的Span发给central

    (3)一直到end都没有非空的桶,则向系统申请一个128页的Span,切分出2页Span和126页的Span,记录好它们各自的页号与Span的映射后,2页的Span给central,126页的Span挂载对应的桶上

​ 如下图演示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fuib2Nob-1676831404638)(G:\Typora\图片保存\image-20230217003517454.png)]

  • 向系统申请的函数

    static const size_t NPAGES = 129;
    static const size_t PAGE_SHIFT = 13;
    #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 ptr;
    }
    
  • Page的对象框架

    Page也属于共享资源,只有一份,所以也是单例模式

    #pragma once
    #include "Common.h"
    class PageCache
    {
    	PageCache(){}
    	PageCache(const PageCache& pc) = delete;
    	PageCache& operator=(PageCache pc) = delete;
    public:
    	static PageCache* GetInstance()
    	{
    		return &_sInst;
    	}
    	//获取一个K页的span
    	Span* NewSpan(size_t k);
    	//获取小块内存与span的映射
    	Span* MapObjectToSpan(void* obj);
    	// 释放空闲span回到Pagecache,并合并相邻的span
    	void ReleaseSpanToPageCache(Span* span);
    
    	std::mutex _mtx;
    private:
    	static PageCache _sInst;
    	SpanList _spanLists[NPAGES];
    	std::unordered_map<PAGE_ID, Span*> _idSpanMap;	//记录页号与Span的映射关系
    };
    
    //static PageCache _sInst;的初始化放在Page.cpp
    

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Iwuk17A2-1676831404639)(G:\Typora\图片保存\image-20230217014427281.png)]

  • 执行部分

    #include "PaageCache.h"
    PageCache PageCache::_sInst;
    //获取一个 新span
    Span* PageCache::NewSpan(size_t k)
    {
    	assert(k > 0);
    	assert( k < NPAGES);
    	//1.先判断申请页的所在的桶是否为NULL(在定义_spanList[]时是定义了129长的数组,所以K不用-1,直接对应桶号即可)
    	if (!_spanLists[k].Empty())
    	{	//(1)不为NULL,从桶里取一个(头删)Span
    		Span* kSpan = _spanLists[k].PopFront();
    		//(2)假如这个Span携带3页,则循环三次,把每页的页号(每页的首地址)都与这个Span建立映射
            //这里可以自行验证下,将地址换成页号后+1再乘8K,就跳过了一页的地址,对于页号的处理方法在central释放部分有介绍
    		for (PAGE_ID i = 0; i < kSpan->_n; i++)
    		{
    			_idSpanMap[kSpan->_pageId + i] = kSpan;
    		}
    		return kSpan;
    	}
    		
    	//2.当对应的桶里没有Span则继续向后面的桶找
    	for (size_t i = k + 1; i < NPAGES; i++)
    	{
            //查看第[i]个桶里是否非NULL
    		if (!_spanLists[i].Empty())
    		{
                //(1)拿到[i]桶里的Span
    			Span* nSpan = _spanLists[i].PopFront();
                //(2)new一个新的空Span为切分做准备
    			Span* kSpan = new Span;
                //(3)给这个KSpan填写页号和页数(central想要的大小页)
    			kSpan->_pageId = nSpan->_pageId;
    			kSpan->_n = k;
    			//(4)更新对nSpan切完后的页号、页数
    			nSpan->_pageId += k;
    			nSpan->_n -= k;
    			//(5)将切完后的Span插入到对应的桶中
    			_spanLists[nSpan->_n].PushFront(nSpan);
    			//(6)记录切完后Span与页号的映射位置,这里不使用的Span只记录开头和结尾页号即可,方便后续合并
    			_idSpanMap[nSpan->_pageId] = nSpan;
    			_idSpanMap[nSpan->_pageId + nSpan->_n - 1] = nSpan;
    			
    			
    			//(7)将要发送给central的Span,建立好它携带页与Span的映射关系(这里和开头部分是一样的)
    			for (PAGE_ID i = 0; i < kSpan->_n; i++)
    			{
    				_idSpanMap[kSpan->_pageId + i] = kSpan;
    			}
    			return kSpan;
    		}
    	}
    	//3.当所有桶都没找到Span后,就向系统申请128页的内存
        //(1)new一个空Span,用来接收这个128页的内存
    	Span* newSpan = new Span;
        //(2)向系统申请,NPAGES是129,要减一
    	void* ptr = SystemAlloc(NPAGES - 1);
        //(3)用首地址算出页号
    	newSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT; //  ptr/8k
        //(4)记录页数
    	newSpan->_n = NPAGES - 1;
        //(5)插入到128页的桶中
    	_spanLists[newSpan->_n].PushFront(newSpan);
        //(6)递归再调用一次自己,需要把这128页的内存切出central想要的页
    	return NewSpan(k);
    
    }
    
    
    //通过内存块,找到对应的page_id,其实本质给central释放回Page时用的
    Span* PageCache::MapObjectToSpan(void* obj)
    {
        //1.根据地址算出页号,一页内的任一地址除8K,CPU只取整,所以都会得到统一的页号
    	PAGE_ID id = (PAGE_ID)obj >> PAGE_SHIFT;
    	//2.加锁,这里的锁是不管是if还是else都先加锁,当作用域退出后自动释放,可以减少冗余代码
        //加锁的原因:这个unordered_map底层是哈希,别的线程在插入、删除时有可能会导致改变结构,所以要加锁
    	std::unique_lock<std::mutex> lock(_mtx);
        //3.查找对应的Span
    	auto ret = _idSpanMap.find(id);
        //若找到了则返回这个Span的地址
    	if (ret != _idSpanMap.end())
    	{
    		return ret->second;
    	}
    	else
    	{
    		//一般来说不可能找不到,因为在page发给central时就已经把span记录在map里了
    		assert(false);
    		return nullptr;
    	}
    }
    

    申请流程如page申请部分开头图示那样所示

  • 这里需要注意:

    注意加锁事项,一般在central调用到page的函数时会加page的锁,也可以不在central调用时加 而是写在page申请函数的开头结尾

    不要遗漏记录Span与页号的映射

    page也是单例模式

释放部分

page释放的流程:

​ central释放函数中,每向page释放一次Span,page拿到Span后都会进行与其他Span的合并检查,本质主要是页合并,最终的会合并到超过128页后还给系统

  • 先检查这个Span携带页的前面的页是否可以合并,若是可以合并,最多可以合并多少页,合并完前面的在去查后面的,然后进行合并

  • 合并完后 再将这个合并完的Span插入到对应的桶中

  • 当合并的页数超过128,则还给系统

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ErR4nMO2-1676831404639)(G:\Typora\图片保存\image-20230217044935306.png)]

//所以对未使用的Span只记录页开头和结尾映射的Span就够了:因为在分配页时是按序分配的,头删或尾删,没有从中间切页的,所以都是按序的整页,对未使用的页只记录开头和结尾的映射就行了
_idSpanMap[nSpan->_pageId] = nSpan;
_idSpanMap[nSpan->_pageId + nSpan->_n - 1] = nSpan;
  • 注意事项:需要在Span添加一个记录使用状态的遍历

    有可能线程A刚分出去一个Span,central刚拿到还未切分,线程B将这个Span给合并了,所以需要增加一个记录使用状态的信息

    class Span
    {
    	PAGE_ID _pageId = 0;		// 大块内存起始页的页号,也就是页的首地址除以8K后的数字
    	size_t  _n = 0;
    	......
    	bool _isUse = false;		//记录使用状态,已分配出去为true
    }
    

    同时在central申请Span代码中,加上更改使用状态的代码

    //获取非空Span
    Span* CentralCache::GetOneSpan(SpanList& list, size_t byte_size)
    {
        .....
        list._mtx.unlock();
    	PageCache::GetInstance()->_mtx.lock();
    	Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(byte_size));
    	span->_isUse = true;		//在这里加上
        ......
    }
    
  • 向系统释放的函数接口,一定是达到128页以后,才可以释放,因为向系统申请时是每次申请128页的

    /***********common.h**************************/
    
    inline static void SystemFree(void* ptr)
    {
    #ifdef _WIN32
    	VirtualFree(ptr, 0, MEM_RELEASE);
    #else
    	// sbrk unmmap等
    #endif
    }
    
  • 代码部分

    //合并
    void PageCache::ReleaseSpanToPageCache(Span* span)
    {
        //1.还回来的Span带的页数超过128页则还给系统
    	if (span->_n > NPAGES - 1)
    	{
            //(1)先将页号恢复成地址
    		void* ptr = (void*)(span->_pageId << PAGE_SHIFT);
            //(2)释放
    		SystemFree(ptr);
            //(3)删除这个span
    		delete span;
    		return;
    	}
    
    	//2.还回来的页数没超过128页,则先向前合并
    	while (true)
    	{
            //(1)先获取前面一页的页号
    		PAGE_ID prevID = span->_pageId - 1;
            //通过页号查找span,判断这个Span是否存在
    		auto ret = _idSpanMap.find(prevID);
    		if (ret == _idSpanMap.end())
    			break;
    		//再判断是否是未使用状态
    		Span* prevspan = ret->second;
    		if (prevspan->_isUse == true)
    			break;
    		//最后判断这两个Span携带的页数加一起是否超过128页 
    		if (span->_n + prevspan->_n > NPAGES - 1)
    			break;
            
    		//经过前面的检查没问题就可以合并	
            //(2)更新首页页号
    		span->_pageId = prevspan->_pageId;
            //(3)更新页数
    		span->_n += prevspan->_n;
    		//(4)删除被合并的Span以及在桶中的位置
    		_spanLists[prevspan->_n].Earse(prevspan);
    		delete prevspan;		
    	}
    	//3.向前合并完后向后合并,基本步骤都一样
    	while (true)
    	{
            //向后合并先要找Span携带最后一页后面的页
    		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 (span->_n + nextspan->_n > NPAGES - 1)
    			break;
    
    		//因为是向后合并,Span的页号不需要变,只改变页数
    		span->_n += nextspan->_n;
    		_spanLists[nextspan->_n].Earse(nextspan);
    		delete nextspan;
    	}
    
        //4.前后都合并完后,将Span插入到对应的桶中
    	_spanLists[span->_n].PushFront(span);
        //更新使用状态
    	span->_isUse = false;
        //重新记录映射关系
    	_idSpanMap[span->_pageId] = span;
    	_idSpanMap[span->_pageId + span->_n - 1] = span;
    }
    

    page的合并细节比较多:

    (1)检查是否达到还给系统的标准

    (2)内存池里的所有Span都有与页建立映射,要检查被合并的页是否存在这个内存池里

    (3)检查被和合并页的Span,携带的页数,是否超过最大值

    (4)合并完后要根据 向前还是向后合并,更新页号与页数,删除被合并页所在的Span,并去除被合并Span在桶中的位置

    (5)对新合并出来的Span重新建立映射关系,在插入到对应桶中

    这里的锁加在central调用这个函数的前后,也可以在函数里面加解

细节优化

大于256KB的申请

thread cache最大只能分配给用户256KB的内存,那么大于256的则直接向Page申请,再由Page向系统申请

  • 对用户申请接口代码优化
static void* ConcurrentAlloc(size_t size)
{
	if (size > MAX_BYTES)
	{
		//1.算对齐数(超出256KB的部分在RoundUp()最后的else中)
		size_t alignSize = SizeClass::RoundUp(size);
		//2.算页数
		size_t kpage = alignSize >> PAGE_SHIFT;
		//3.向page申请,由page new一个span再挂载内存 
		PageCache::GetInstance()->_mtx.lock();
		Span* span = PageCache::GetInstance()->NewSpan(kpage);
		PageCache::GetInstance()->_mtx.unlock();
		//4.发给用户
		void* ptr = (void*)(span->_pageId << PAGE_SHIFT);
		return ptr;

	}
	else//小于256KB的 申请
	{
		if (pTLS_threadCache == nullptr)
		{
			pTLS_threadCache = new ThreadCache;
		}
		return pTLS_threadCache->Allocate(size);
	}
}
  • 对page的newSpan函数更改

    Span* PageCache::NewSpan(size_t k)
    {
    	assert(k > 0);
    
    	if (k > NPAGES - 1)
    	{
            //1.向系统申请
    		void* ptr = SystemAlloc(k);
            //2.new一个空Span
    		Span* span = new Span;
    		//3.填写页号与页数
    		span->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
    		span->_n = k;
    		//4.记录好映射后发给用户
    		_idSpanMap[span->_pageId] = span;
    		return span;
    	}
        
        assert( k < NPAGES);
    	if (!_spanLists[k].Empty())
        ......
            
    }
    

大于256KB的申请只是由Page代申请,并记录一下映射方便释放时查找,不会影响小于256KB部分的合并

  • 释放部分

    原来释放的接口需要用户传入对应的对齐数,太麻烦了,这里把 这个接口优化掉。

    具体做法:在class Span中多加一个参数,用来记录对齐数,之后在分配给用户之前只要填上这个对齐数即可

    1. class Span部分

      struct Span
      {
      	PAGE_ID _pageId = 0;		// 大块内存起始页的页号
      	size_t  _n = 0;				// 页的数量
      	......
      	size_t _objsize = 0;
      };
      
    2. 添加对齐数,一共两处需要更改,(在central向page获取Span的函数中,用户申请超256KB处)

      Span* CentralCache::GetOneSpan(SpanList& list, size_t byte_size)
      {
          .....
          list._mtx.unlock();
      	PageCache::GetInstance()->_mtx.lock();
      	Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(byte_size));
      	span->_isUse = true;	
          span->_objsize = byte_size;				//在这里加上,byte_size就是thread算好的对齐数传上来的
          ......
      }
          
      static void* ConcurrentAlloc(size_t size)
      {
      	if (size > MAX_BYTES)
      	{
              ......
      		PageCache::GetInstance()->_mtx.lock();
      		Span* span = PageCache::GetInstance()->NewSpan(kpage);
      		span->_objsize = alignSize;//在这里添加
              ......
      }
      

    在用户释放时就可以像free一样只传首地址即可

    static void ConcurrentFree(void* ptr)
    {
        //1.找到ptr所属Span
    	Span* span = PageCache::GetInstance()->MapObjectToSpan(ptr);
        //2.获取它的对齐数
    	size_t size = span->_objsize;
    	//大于256KB处理的部分
    	if (size > MAX_BYTES)
    	{
            //传给Page,由Page直接还给系统
    		PageCache::GetInstance()->_mtx.lock();
    		PageCache::GetInstance()->ReleaseSpanToPageCache(span);
    		PageCache::GetInstance()->_mtx.unlock();
    	}
    	else//小于256KB处理的部分
    	{
    		assert(pTLS_threadCache);
    
    		pTLS_threadCache->Deallocate(ptr, size);
    	}
    }
    

使用定长内存池代替new Span

在这个内存池中,需要频繁的new Span 、deltet Span,所以对于这部分可以使用定长内存池替代

具体步骤:

  1. 在Page的框架中添加定长内存池的成员变量

    class PageCache
    {
    public:
    	......
    private:
    	static PageCache _sInst;
    	SpanList _spanLists[NPAGES];
        std::unordered_map<PAGE_ID, Span*> _idSpanMap;
    	ObjectPool<Span> _spanPool;//定长内存池成员变量
    };
    
  2. 替换所有new、delete的部分(基本集中在page部分)

    Span* PageCache::NewSpan(size_t k)函数中有三处newvoid PageCache::ReleaseSpanToPageCache(Span* span)函数中有三处delete
        
    
  3. 在每个用户申请thread时,也会用到new,所以还要添加一个 thread的定长内存池

    else
    	{
    		// 通过TLS 每个线程无锁的获取自己的专属的ThreadCache对象
    		if (pTLSThreadCache == nullptr)
    		{
    			static ObjectPool<ThreadCache> tcPool;		//设置为静态,保证每个线程只申请一次
    			//pTLSThreadCache = new ThreadCache;
    			pTLSThreadCache = tcPool.New();
    		}
    		return pTLSThreadCache->Allocate(size);
    	}	
    }
    

整体代码

  • 用户接口( ConcurrentAllocl.h)
#pragma once

#include "Common.h"
#include "thread_cache.h"
#include "PaageCache.h"
#include "ObjectPool.h"

static void* ConcurrentAlloc(size_t size)
{
	if (size > MAX_BYTES)
	{
		//1.算对齐数(超出256KB的部分在RoundUp()最后的else中)
		size_t alignSize = SizeClass::RoundUp(size);
		//2.算页数
		size_t kpage = alignSize >> PAGE_SHIFT;
		//3.向page申请,由page new一个span再挂载内存 
		PageCache::GetInstance()->_mtx.lock();
		Span* span = PageCache::GetInstance()->NewSpan(kpage);
		PageCache::GetInstance()->_mtx.unlock();
		//4.发给用户
		void* ptr = (void*)(span->_pageId << PAGE_SHIFT);
		return ptr;

	}
	else//小于256KB的 申请
	{
		if (pTLS_threadCache == nullptr)
		{
			static ObjectPool<ThreadCache> tcPool;		//设置为静态,保证每个线程只申请一次
			//pTLS_threadCache = new ThreadCache;
			pTLS_threadCache = tcPool.New();
		}
		return pTLS_threadCache->Allocate(size);
	}
}



static void ConcurrentFree(void* ptr)
{
    //1.找到ptr所属Span
	Span* span = PageCache::GetInstance()->MapObjectToSpan(ptr);
    //2.获取它的对齐数
	size_t size = span->_objsize;
	//大于256KB处理的部分
	if (size > MAX_BYTES)
	{
        //传给Page,由Page直接还给系统
		PageCache::GetInstance()->_mtx.lock();
		PageCache::GetInstance()->ReleaseSpanToPageCache(span);
		PageCache::GetInstance()->_mtx.unlock();
	}
	else//小于256KB处理的部分
	{
		assert(pTLS_threadCache);
		pTLS_threadCache->Deallocate(ptr, size);
	}
}
  • Common.h

    /*********************Common.h*********************/
    #pragma once
    #include <iostream>
    #include <vector>
    #include <time.h>
    #include <assert.h>
    #include <thread>
    #include <algorithm>
    #include <mutex>
    #include <unordered_map>
    #include <atomic>
    
    #ifdef _WIN64
    typedef unsigned long long PAGE_ID;
    #elif _WIN32
    typedef size_t PAGE_ID;
    #endif
    
    using std::cout;
    using std::endl;
    using std::bad_alloc;
    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 _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 ptr;
    }
    
    inline static void SystemFree(void* ptr)
    {
    #ifdef _WIN32
    	VirtualFree(ptr, 0, MEM_RELEASE);
    #else
    	// sbrk unmmap等
    #endif
    }
    
    
    static void*& NextObj(void* obj)
    {
    	return *(void**)obj;
    }
    
    
    //管理thread_ache中切分好的小块对象(内存),方便挂载到哈希桶
    class FreeList
    {
    public:
    	void Push(void* obj)
    	{
    		assert(obj);
    		//头插
    		NextObj(obj) = _freeList;
    		_freeList = obj;
    		_size += 1;
    
    	}
    
    	void* Pop()
    	{
    		assert(_freeList);
    		//头删
    		void* obj = _freeList;
    		_freeList = NextObj(_freeList);
    		_size -= 1;
    
    		return obj;
    	}
    
    
    	bool Empty()
    	{
    		return _freeList == nullptr;
    	}
    
    	size_t& MaxSize()
    	{
    		return _maxSize;
    	}
    
    	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)
    	{
    		start = _freeList;
    		end = start;
    		for (size_t i = 0; i < n - 1; i++)
    			end = NextObj(end);
    
    		_freeList = NextObj(end);
    		NextObj(end) = nullptr;	
    		_size -= n;
    	}
    
    	size_t Size()
    	{
    		return _size;
    	}
    private:
    	void* _freeList = nullptr;//头指针
    	size_t _maxSize = 1;
    	size_t _size = 0; //记录每个链表上的内存块个数,用于释放判断
    };
    
    
    // 管理多个连续页大块内存跨度结构
    struct Span
    {
    	PAGE_ID _pageId = 0;		// 大块内存起始页的页号
    	size_t  _n = 0;				// 页的数量
    
    	Span* _next = nullptr;		// 双向链表的结构——>尾指针
    	Span* _prev = nullptr;		// 双向链表的结构——>头指针
    
    	size_t _useCount = 0;		// 切好小块内存,被分配给thread cache的计数,每分走一个就+1
    	void* _freeList = nullptr;  // 切好的小块内存的自由链表
    
    	bool _isUse = false;		//记录使用状态,用于合并
    	size_t _objsize = 0;		//记录对齐数,方便用户释放接口
    };
    
    
    
    class SpanList
    {
    public:
    	SpanList()
    	{
    		_head = new Span;
    		_head->_prev = _head;
    		_head->_next = _head;
    	}
    	//指定位置的头插一个Span
    	void Insert(Span* pos, Span* newSpan)
    	{
    		assert(pos && newSpan);
    
    		Span* newSpan_prev = pos->_prev;
    
    		newSpan->_next = pos;
    		newSpan->_prev = newSpan_prev;
    		newSpan_prev->_next = newSpan;
    		pos->_prev = newSpan;
    	}
    	//指定位置的头删一个Spaan
    	void Earse(Span* pos)
    	{
    		assert(pos);
    		assert(pos != _head);
    
    		Span* pos_next = pos->_next;
    		Span* pos_prev = pos->_prev;
    
    		pos_next->_prev = pos_prev;
    		pos_prev->_next = pos_next;
    	}
    	//SpanList的第一个Span
    	Span* Begin()
    	{
    		return _head->_next;
    	}
        //SpanList的最后一个Span
    	Span* End()
    	{
    		return _head;
    	}
        //判断SpanList是否为 NULL
    	bool Empty()
    	{
    		return _head == _head->_next;
    	}
    	//头插
    	void PushFront(Span* span)
    	{
    		Insert(Begin(),span);
    	}
    	//头删
    	Span* PopFront()
    	{
    		Span* Front = _head->_next;
    		Earse(Front);
    		return Front;
    	}
    
    	std::mutex _mtx; // 桶锁
    private:
    	Span* _head;	//头指针
    
    };
    
    
    
    
    //计算对齐数、下标、thread向central获取的内存块、centraal向page获取的页数
    class SizeClass
    {
    	// 整体控制在最多10%左右的内碎片浪费
    	// [1,128]						8byte对齐			freelist[0,16)		------16个桶
    	// [128+1,1024]					16byte对齐			freelist[16,72)		------56个桶
    	// [1024+1,8*1024]				128byte对齐			freelist[72,128)	------56个桶
    	// [8*1024+1,64*1024]			1024byte对齐		freelist[128,184)	------56个桶
    	// [64*1024+1,256*1024]			8*1024byte对齐		freelist[184,208)	------24个桶
    
    	//例如129对齐是144,浪费15个字节,15/144 = 0.104...
    	//前1-56浪费率会高,但是前几个对齐的字节数小,可以忽略
    
    
    
    
    	//计算对齐数,例如9字节,对齐数就是16
    	/*方法1:
    	inline static size_t _RoundUp(size_t size,size_t alignNum)
    	{
    		size_t alignSize;
    		if (size % alignNum != 0)
    		{
    			alignSize = (size / alignNum + 1) * alignNum;// 9 % 8=1 + 1 =2 * 8 =16
    		}
    		else
    		{
    			alignSize = alignNum;
    		}
    		return alignSize;
    	}*/
    	//方法2:
    	inline static size_t _RoundUp(size_t bytes, size_t alignNum)
    	{
    		return ((bytes + alignNum - 1) & ~(alignNum - 1));
    	}
    
    
    	//计算哈希桶号,也就是哈希映射数组的下标
    	/*方法1:
    	size_t _Index(size_t bytes, size_t alignNum)
    	{
    		if (bytes % alignNum == 0)
    		{
    			return bytes / alignNum - 1;
    		}
    		else
    		{
    			return bytes / alignNum;
    		}
    	}*/
    	//方法2:
    	static inline size_t _Index(size_t bytes, size_t align_shift)	//9,3
    	{
    		return ((bytes + (1 << align_shift) - 1) >> align_shift) - 1; //9 + (1 << 3) - 1 = 8 - 1 + 9 = 16
    	}																  //16 >> 3 - 1 = 1
    
    
    
    public:
    	//计算thred_cache用户申请字节的对齐数 
    	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
    		{
    			return _RoundUp(size, 1 << PAGE_SHIFT);	
    		}
    
    	}
    
    
    	//计算哈希桶的哪一个桶(映射的数组下标所挂载的自由链表)
    	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)
    		{																		//bytes - 128:当前区间的字节数 - 上一区间的总字节数
    			return _Index(bytes - 128, 4) + group_array[0];						//group_array[0]:1~128区间的总桶数量
    		}
    		else if (bytes <= 8 * 1024)
    		{
    			return _Index(bytes - 1024, 7) + group_array[1] + group_array[0];	//group_array[1] + group_array[0] = 1~1024间的总桶数
    		}
    		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;
    		}	
    	}
    
    	// 每一次thread cache要从中心缓存获取多少个内存块
    	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;
    	}
    
    private:
    
    };
    
    
    
  • thread cache .h/.cpp

    #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);
    
    	//用户释放时,单个桶过长(释放的超过批量申请的),则释放一些给central_cache
    	void ListTooLong(FreeList& list, size_t size);
    private:
    	FreeList _freeList[NFREELIST];//数组长208,也就是有208个桶,即挂载208个自由链表
    };
    
    
    
    //每个线程各自持有一个
    static _declspec(thread) ThreadCache* pTLS_threadCache = nullptr;
    /*
    线程局部存储(TLS),是一种变量存储的方法,这个变量在它所在线程内是全局可访问的,但不能被其他线程访问到
    */
    
    
    
    
    
    
    
    /*************************thread.cpp*****************/
    #include "thread_cache.h"
    #include "CentraalCache.h"
    //用户申请
    void* ThreadCache::Allocate(size_t size)
    {
    	assert(size <= MAX_BYTES);
    	//1.计算用户申请的字节对应的对齐数
    	size_t allignSize = SizeClass::RoundUp(size);
    	//2.计算用户要到哪个桶取内存块(数组映射的下标中挂载的链表取内存块)
    	size_t index = SizeClass::Index(size);
    
    	//判断index下标的FreeList中的头指针是否为NULL
    	if (!_freeList[index].Empty())
    	{
    		return _freeList[index].Pop();					//不为NULL就可以Pop给用户了
    	}
    	else
    	{
    		return FetchFromCentralCache(index,allignSize);//为NULL就需要向中心缓存申请
    	}
    }
    
    
    
    //当用户申请时thread没有,则向第二层申请
    void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
    {
        //thread申请只会每次申请1个,但是central可以每次多给一些,就不用频繁申请了,所以这里采用慢启动调节分配个数
        
    	//1.慢启动调节
    	//(1)最开始不会一次向central 批给cache太多,可能会用不完
    	//(2)会从1个内存块开始递增给它(batchNum),每申请一次下次就会多一个,直到与算法算的数值持平,再改用算法给出的数值
    	//(3)相关算法给出的数值:要的字节数越大,给cache的内存块就越少,要的字节数越小,给cache批的内存块就越多
    	size_t batchNum = min(_freeList[index].MaxSize(), SizeClass::NumMoveSize(size));	
    	if (batchNum == _freeList[index].MaxSize())
    	{
    		_freeList[index].MaxSize() += 1;
    	}
        
    	//2.向第二层申请
        //(1)参数设置:输出型参数,去第二层获取内存块组成链表的头和尾的地址
    	void* start = nullptr;
    	void* end = nullptr;
    	//(2)向第二层的哈希桶内获取批量内存块(参数:头部位置,结尾位置,申请的个数,申请的字节大小)(第二层函数接口)
    	size_t actualNum = CentralCache::GetInstance()->FetchRangeObj(start, end, batchNum, size);
    	//(3)根据返回值判断获取的个数,给的返回值(actualNum)不一定要多少就能拿到多少,例如要4个但只剩2个,就只能返还2个
    	assert(actualNum > 0);
    	
    	if (actualNum == 1)					//假如就剩1个,直接给用户
    	{
    		assert(start == end);
    		return start;
    	}
    	else								//超过一个则留下头一个,其他的批量头插到thread的哈希桶的自由链表里
    	{
    		_freeList[index].PushRange(NextObj(start), end, actualNum - 1);
    		return start;
    	}
    }
    
    
    
    //用户释放
    void ThreadCache::Deallocate(void* ptr, size_t size)
    {
    	assert(ptr);
    	assert(size <= MAX_BYTES);
    	//1.计算释放回来的字节数对应的桶,并头插
    	size_t index = SizeClass::Index(size);
    	_freeList[index].Push(ptr);
    	//2.判断该桶挂载的内存块个数,是否达到归还第二层的个数标准
    	if (_freeList[index].Size() > _freeList[index].MaxSize())
    	{
    		ListTooLong(_freeList[index], size);
    	}
    }
    //释放给第二层
    void ThreadCache::ListTooLong(FreeList& list, size_t size)
    {
        //1.参数设置:输出型参数
    	void* start = nullptr;
    	void* end = nullptr;
    	//2.批量头删:取MaxSize()个,并拿到取到的 整个自由链表的 头地址和尾地址
    	list.PopRange(start, end, list.MaxSize());
    	//3.释放给第二层 central cache(第二层的函数接口)
    	CentralCache::GetInstance()->ReleaseListToSpans(start, size);
    }
    
    
    
  • central cache.h/.cpp

    #pragma once
    #include "Common.h"
    
    
    //单例模式——饿汉模式
    class CentralCache
    {
    	CentralCache(){}
    	CentralCache(const CentralCache& cc) = delete;
    	CentralCache& operator=(CentralCache cc) = delete;
    public:
    
    	static CentralCache* GetInstance()
    	{
    		return &_sInst;
    	}
    	
    	//期望批量从span获取内存块给thread
    	size_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size);
    	//获取一个非空的Span
    	Span* GetOneSpan(SpanList& list,size_t byte_size);
    	// 将一定数量的对象释放到span跨度
    	void ReleaseListToSpans(void* start, size_t byte_size);
    private:
    	SpanList _spanList[NFREELIST];
    	static CentralCache _sInst;
    };
    
    
    
    /*************************central.cpp*****************/
    #include "CentraalCache.h"
    #include "Common.h"
    #include "PaageCache.h"
    
    CentralCache CentralCache::_sInst;
    //从central获取内存块给thread
    //这里参数中的start end就是thread中调用FetchFromCentralCache函数时的start end
    size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size)
    {
    	//1.算一下在central中的哈希桶号(数组下标)
    	size_t index = SizeClass::Index(size);
    	
        //2.指定index号桶 加锁
    	_spanList[index]._mtx.lock();
    	
        //3.获取index号桶中的非空span
    	Span* span = GetOneSpan(_spanList[index],size);
    	assert(span);
    	assert(span->_freeList);
    
    	//4.遍历这个span,数出需要多少个内存块
    	start = span->_freeList;		
    	end = start;
    	//start此时指向的就是第一个,actualNum = 1
    	size_t actualNum = 1;
    	//span中的内存块也可能达不到预期的个数,所以碰到nullptr也要终止遍历
    	while (actualNum < batchNum && NextObj(end) != nullptr)
    	{
    		end = NextObj(end);	
    		actualNum++;	
    	}
        
    	//5.更新相关的属性
        
        //(1)更新头指针的指向
        //例如要2个,那么end的位置正好就在第二个位置,所以头指针指向的应该是end的下一个
    	span->_freeList = NextObj(end);	
    	//(2)更新end的next指向
    	NextObj(end) = nullptr;			
    	//(3)更新拿走多少个小块内存
    	span->_useCount += actualNum;
    	//到这里,这个span已经删掉了actualNum个,采用的是在指定位置尾删
        
        //6.解除index号桶的锁
    	_spanList[index]._mtx.unlock();
    	//返回给thread的个数
    	return actualNum;
    }
    
    
    
    //获取一个非空的Span
    Span* CentralCache::GetOneSpan(SpanList& list, size_t byte_size)
    {
    	//1.先找到非空的Span
        //(1)从第一个开始判断,中途有非空的直接返回Span的地址,没有则继续往下找,直到全都没有
    	Span* it = list.Begin();
    	while (it != list.End())
    	{
    		if (it->_freeList != nullptr)
    			return it;
    		else
    			it = it->_next;
    	}
        //(2)全都没有则向page申请,此时本线程转到page中,但是其他线程可能会释放需要进入这个桶,所以这里先解锁
    	list._mtx.unlock();
        
    	//2.循环结束没有找到,则向Page索要
        //(1)page也属于共享资源所以也要加它的锁,page部分具体在后面介绍
    	PageCache::GetInstance()->_mtx.lock();
        //(2)先根据这个字节数计算需要多少页,之后再申请
    	Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(byte_size));
        span->_isUse = true;		//已使用状态
         span->_objsize = byte_size;//此Span要切分内存块的的对齐数	
        //(3)解page的锁
    	PageCache::GetInstance()->_mtx.unlock();
    	
        //3.拿到Page给的Span,开始按申请的字节数(byte_size)切分
        //(1)将此Span记录的页号乘8K,恢复到16位首地址的表示方式,之后强转为char*,方便切分
    	char* start = (char*)(span->_pageId << PAGE_SHIFT);
        //(2)将页的个数乘8k,计算一共有多少个8k,也就是所有页加一起的总字节数
    	size_t bytes = span->_n << PAGE_SHIFT;
    	//(3)记录这个Span携带所有页总和的末尾地址
    	char* end = start + bytes;
        //(4)开始切分,先更新头指针指向
    	span->_freeList = start;
    	start += byte_size;
        //(5)开始遍历,start经过byte_size个字节跳转到下一位置,tail的next指向start的位置,完成一次切分
    	void* tail = span->_freeList;
        //采用的是尾插法,这样可以让内存尽量连续,提高缓存利用率
    	while (start < end)	
    	{
    		NextObj(tail) = start;
    		tail = start;//tail = NextObj(tail);	
    		start += byte_size;
    	}
        //(6)切分完成后,设置末尾的内存块的next指向NULL
    	NextObj(tail) = nullptr;
        
        //4.向page申请内存的部分结束,重新去争锁,因为要访问桶,把刚获得的Span挂载到central的桶上
    	list._mtx.lock();
        //5.把刚切完的内存块挂载到这个Span的头指针上
    	list.PushFront(span);
    	//6.返回刚获得并切完的Span
    	return span;
    }
    
    void CentralCache::ReleaseListToSpans(void* start, size_t byte_size)
    {
    	//1.算桶号
    	size_t index = SizeClass::Index(byte_size);
    	//2.加锁,只要访问到共享资源就需要加锁,而释放就是在Span中插入内存块,所以是访问共享资源,需要加锁
    	_spanList[index]._mtx.lock();
    	//3.因为返回的批量内存块,不可能都属于同一个Span,所以对每个内存块都查找一次
    	while (start)
    	{
            //(1)记录next的位置,方便后续查找
    		void* next = NextObj(start);
            //(2)对每个内存块,都查找对应的Span,此函数接口是page中的(根据地址获取对应的Span)
    		Span* span = PageCache::GetInstance()->MapObjectToSpan(start);
    		//(3)找到对应的Span后,头插
    		NextObj(start) = span->_freeList;
    		span->_freeList = start;
            //(4)更新这个Span记录分走内存块的个数
    		span->_useCount--;
    		//(5)当一个span所有的内存块都回来后,将这个Span释放给Page
    		if (span->_useCount == 0)
    		{
    			_spanList[index].Earse(span);	//删除这个Span
    			span->_next = nullptr;			//清除前后指针记录的位置,防止发生越界访问等错误
    			span->_prev = nullptr;			//......
    			span->_freeList = nullptr;		//既然这个页的内存块都已回收,就可以不用头指针记录了,有页号就够了
    
                //前面都已经清除这个Span在桶中的关联了,再向page释放Span时就可以暂时解锁了
    			_spanList[index]._mtx.unlock();	
                //page也是只有一个也是共享的,central在向page释放page时,是在访问page这个共享资源,所以需要加page相关的锁
    			PageCache::GetInstance()->_mtx.lock();
                //page的函数接口,功能是把centraal释放的span插入到page相关的结构中
    			PageCache::GetInstance()->ReleaseSpanToPageCache(span);
                //解page的锁,争central的桶锁
    			PageCache::GetInstance()->_mtx.unlock();
    			_spanList[index]._mtx.lock();
    		}
            //4.当前内存块找完,准备找下一个
    		start = next;
    	}
    	//解锁
    	_spanList[index]._mtx.unlock();
    }
    
    
  • page cache .h/.cpp

    #pragma once
    #include "Common.h"
    #include "ObjectPool.h"
    class PageCache
    {
    	PageCache(){}
    	PageCache(const PageCache& pc) = delete;
    	PageCache& operator=(PageCache pc) = delete;
    public:
    	static PageCache* GetInstance()
    	{
    		return &_sInst;
    	}
    	//获取一个K页的span
    	Span* NewSpan(size_t k);
    	//获取小块内存与span的映射
    	Span* MapObjectToSpan(void* obj);
    
    	// 释放空闲span回到Pagecache,并合并相邻的span
    	void ReleaseSpanToPageCache(Span* span);
    
    	std::mutex _mtx;
    
    private:
    	static PageCache _sInst;
    	SpanList _spanLists[NPAGES];
    	ObjectPool<Span> _spanPool;
    	std::unordered_map<PAGE_ID, Span*> _idSpanMap;
    };
    
    
    /************************page.cpp************************/
    
    #include "PageCache.h"
    PageCache PageCache::_sInst;
    //获取一个 新span
    Span* PageCache::NewSpan(size_t k)
    {
    	assert(k > 0);
    
    	if (k > NPAGES - 1)
    	{
            //1.向系统申请
    		void* ptr = SystemAlloc(k);
            //2.new一个空Span
    		//Span* span = new Span;
            Span* span = _spanPool.New();
    		//3.填写页号与页数
    		span->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
    		span->_n = k;
    		//4.记录好映射后发给用户
    		_idSpanMap[span->_pageId] = span;
    		return span;
    	}
        
    	assert( k < NPAGES);
    	//1.先判断申请页的所在的桶是否为NULL(在定义_spanList[]时是定义了129长的数组,所以K不用-1,直接对应桶号即可)
    	if (!_spanLists[k].Empty())
    	{	//(1)不为NULL,从桶里取一个(头删)Span
    		Span* kSpan = _spanLists[k].PopFront();
    		//(2)假如这个Span携带3页,则循环三次,把每页的页号(每页的首地址)都与这个Span建立映射
            //这里可以自行验证下,将地址换成页号后+1再乘8K,就跳过了一页的地址,对于页号的处理方法在central释放部分有介绍
    		for (PAGE_ID i = 0; i < kSpan->_n; i++)
    		{
    			_idSpanMap[kSpan->_pageId + i] = kSpan;
    		}
    		return kSpan;
    	}
    		
    	//2.当对应的桶里没有Span则继续向后面的桶找
    	for (size_t i = k + 1; i < NPAGES; i++)
    	{
            //查看第[i]个桶里是否非NULL
    		if (!_spanLists[i].Empty())
    		{
                //(1)拿到[i]桶里的Span
    			Span* nSpan = _spanLists[i].PopFront();
                //(2)new一个新的空Span为切分做准备
    			//Span* kSpan = new Span;
                Span* KSpan = _spanPool.New();
                //(3)给这个KSpan填写页号和页数(central想要的大小页)
    			kSpan->_pageId = nSpan->_pageId;
    			kSpan->_n = k;
    			//(4)更新对nSpan切完后的页号、页数
    			nSpan->_pageId += k;
    			nSpan->_n -= k;
    			//(5)将切完后的Span插入到对应的桶中
    			_spanLists[nSpan->_n].PushFront(nSpan);
    			//(6)记录切完后Span与页号的映射位置,这里不使用的Span只记录开头和结尾页号即可,方便后续合并
    			_idSpanMap[nSpan->_pageId] = nSpan;
    			_idSpanMap[nSpan->_pageId + nSpan->_n - 1] = nSpan;
    			
    			
    			//(7)将要发送给central的Span,建立好它携带页与Span的映射关系(这里和开头部分是一样的)
    			for (PAGE_ID i = 0; i < kSpan->_n; i++)
    			{
    				_idSpanMap[kSpan->_pageId + i] = kSpan;
    			}
    			return kSpan;
    		}
    	}
    	//3.当所有桶都没找到Span后,就向系统申请128页的内存
        //(1)new一个空Span,用来接收这个128页的内存
    	//Span* newSpan = new Span;
        Span* newSpan = _spanPool.New();
        //(2)向系统申请,NPAGES是129,要减一
    	void* ptr = SystemAlloc(NPAGES - 1);
        //(3)用首地址算出页号
    	newSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT; //  ptr/8k
        //(4)记录页数
    	newSpan->_n = NPAGES - 1;
        //(5)插入到128页的桶中
    	_spanLists[newSpan->_n].PushFront(newSpan);
        //(6)递归再调用一次自己,需要把这128页的内存切出central想要的页
    	return NewSpan(k);
    
    }
    
    
    //通过内存块,找到对应的page_id,其实本质给central释放回Page时用的
    Span* PageCache::MapObjectToSpan(void* obj)
    {
        //1.根据地址算出页号,一页内的任一地址除8K,CPU只取整,所以都会得到统一的页号
    	PAGE_ID id = (PAGE_ID)obj >> PAGE_SHIFT;
    	//2.加锁,这里的锁是不管是if还是else都先加锁,当作用域退出后自动释放,可以减少冗余代码
        //加锁的原因:这个unordered_map底层是哈希,别的线程在插入、删除时有可能会导致改变结构,所以要加锁
    	std::unique_lock<std::mutex> lock(_mtx);
        //3.查找对应的Span
    	auto ret = _idSpanMap.find(id);
        //若找到了则返回这个Span的地址
    	if (ret != _idSpanMap.end())
    	{
    		return ret->second;
    	}
    	else
    	{
    		//一般来说不可能找不到,因为在page发给central时就已经把span记录在map里了
    		assert(false);
    		return nullptr;
    	}
    }
    
    
    //合并
    void PageCache::ReleaseSpanToPageCache(Span* span)
    {
        //1.还回来的Span带的页数超过128页则还给系统
    	if (span->_n > NPAGES - 1)
    	{
            //(1)先将页号恢复成地址
    		void* ptr = (void*)(span->_pageId << PAGE_SHIFT);
            //(2)释放
    		SystemFree(ptr);
            //(3)删除这个span
    		//delete span;
            _spanPool.Delete(span);
    		return;
    	}
    
    	//2.还回来的页数没超过128页,则先向前合并
    	while (true)
    	{
            //(1)先获取前面一页的页号
    		PAGE_ID prevID = span->_pageId - 1;
            //通过页号查找span,判断这个Span是否存在
    		auto ret = _idSpanMap.find(prevID);
    		if (ret == _idSpanMap.end())
    			break;
    		//再判断是否是未使用状态
    		Span* prevspan = ret->second;
    		if (prevspan->_isUse == true)
    			break;
    		//最后判断这两个Span携带的页数加一起是否超过128页 
    		if (span->_n + prevspan->_n > NPAGES - 1)
    			break;
            
    		//经过前面的检查没问题就可以合并	
            //(2)更新首页页号
    		span->_pageId = prevspan->_pageId;
            //(3)更新页数
    		span->_n += prevspan->_n;
    		//(4)删除被合并的Span以及在桶中的位置
    		_spanLists[prevspan->_n].Earse(prevspan);
    		//delete prevspan;		
            _spanPool.Delete(prevspan);
    	}
    	//3.向前合并完后向后合并,基本步骤都一样
    	while (true)
    	{
            //向后合并先要找Span携带最后一页后面的页
    		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 (span->_n + nextspan->_n > NPAGES - 1)
    			break;
    
    		//因为是向后合并,Span的页号不需要变,只改变页数
    		span->_n += nextspan->_n;
    		_spanLists[nextspan->_n].Earse(nextspan);
    		//delete nextspan;
            _spanPool.Delete(nextspan);
    	}
    
        //4.前后都合并完后,将Span插入到对应的桶中
    	_spanLists[span->_n].PushFront(span);
        //更新使用状态
    	span->_isUse = false;
        //重新记录映射关系
    	_idSpanMap[span->_pageId] = span;
    	_idSpanMap[span->_pageId + span->_n - 1] = span;
    }
    
  • 定长内存池(ObjectPool.h)

    //合并
    void PageCache::ReleaseSpanToPageCache(Span* span)
    {
        //1.还回来的Span带的页数超过128页则还给系统
    	if (span->_n > NPAGES - 1)
    	{
            //(1)先将页号恢复成地址
    		void* ptr = (void*)(span->_pageId << PAGE_SHIFT);
            //(2)释放
    		SystemFree(ptr);
            //(3)删除这个span
    		delete span;
    		return;
    	}
    
    	//2.还回来的页数没超过128页,则先向前合并
    	while (true)
    	{
            //(1)先获取前面一页的页号
    		PAGE_ID prevID = span->_pageId - 1;
            //通过页号查找span,判断这个Span是否存在
    		auto ret = _idSpanMap.find(prevID);
    		if (ret == _idSpanMap.end())
    			break;
    		//再判断是否是未使用状态
    		Span* prevspan = ret->second;
    		if (prevspan->_isUse == true)
    			break;
    		//最后判断这两个Span携带的页数加一起是否超过128页 
    		if (span->_n + prevspan->_n > NPAGES - 1)
    			break;
            
    		//经过前面的检查没问题就可以合并	
            //(2)更新首页页号
    		span->_pageId = prevspan->_pageId;
            //(3)更新页数
    		span->_n += prevspan->_n;
    		//(4)删除被合并的Span以及在桶中的位置
    		_spanLists[prevspan->_n].Earse(prevspan);
    		delete prevspan;		
    	}
    	//3.向前合并完后向后合并,基本步骤都一样
    	while (true)
    	{
            //向后合并先要找Span携带最后一页后面的页
    		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 (span->_n + nextspan->_n > NPAGES - 1)
    			break;
    
    		//因为是向后合并,Span的页号不需要变,只改变页数
    		span->_n += nextspan->_n;
    		_spanLists[nextspan->_n].Earse(nextspan);
    		delete nextspan;
    	}
    
        //4.前后都合并完后,将Span插入到对应的桶中
    	_spanLists[span->_n].PushFront(span);
        //更新使用状态
    	span->_isUse = false;
        //重新记录映射关系
    	_idSpanMap[span->_pageId] = span;
    	_idSpanMap[span->_pageId + span->_n - 1] = span;
    }
    

性能测试及优化

性能测试

测试代码如下:

分别对内存池与malloc测试

#include"ConcurrentAllocl.h"

// ntimes 一轮申请和释放内存的次数
// rounds 轮次
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",
		nworks, rounds, ntimes, malloc_costtime);

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

	printf("%u个线程并发malloc&free %u次,总计花费:%u ms\n",
		nworks, nworks*rounds*ntimes, 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]);
					//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轮次,每轮次concurrent alloc %u次: 花费:%u ms\n",
		nworks, rounds, ntimes, malloc_costtime);

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

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

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

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

	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TDFlb4RW-1676831404639)(G:\Typora\图片保存\image-20230217194454745.png)]

测试结果对比发现,内存池实际效率比malloc慢很多,通过vs自带的性能检测工具查看:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FYVMhWRl-1676831404640)(G:\Typora\图片保存\image-20230217200216337.png)]

由上图可知,对于锁的竞争才是最消耗性能的,且基本都是page的锁

优化

由于 unordered_map的底层是哈希,当达到负载因子后会变换结构,所以可以 用其他结构替换掉它;

​ 因此,这里参考tcmalloc提供的基数树来进行性能的优化。32位下可以采用一层基数树或两层基数树,64位下必须用三层,下面以32位为例介绍:

  • 基于一层的基数树,是直接定值法,32位的地址空间可分配的总页数是:2^32 / 2^13 = 2^19 (524288)个页,所以定义一个长为2^19的数组,数组的类型是指针,用来存Span。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Je9D11BF-1676831404640)(G:\Typora\图片保存\image-20230217211721594.png)]

    template <int BITS>//BITS表示需要多少bit位,32位下需要19位
    class TCMalloc_PageMap1 {
    private:
    	static const int LENGTH = 1 << BITS;//页的数目,数组长度
    	void** array_;//存储映射关系的数组
    

    本质还是哈希,只不过是直接定值,做了最坏的打算,映射所有页号与Span,总大小:2^19 * 4B = 2M

  • 基于两层的基数树,分两次映射,可以理解为高为2的多差树

    第一层一共需要32个槽位,也就是2^5,每一个槽位都映射着一个第二层的指针数组,说明第二层一共有32个数组

    第二层最大有32个数组,每个数组是一个2^14的指针数组,分配规则如下图:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xOUOYywb-1676831404640)(G:\Typora\图片保存\image-20230217230350728.png)]

    ​ 本质来说,第一层是个索引,用来找到第二层的数组;页号的14~18位是用来找到Span在第二层的哪个数组的钥匙,页号的0 ~ 13位是找到第二层数组映射Span的钥匙

    两层比一层的优势

    • 一层基数树必须上来就开好所有内存,而两层基数树,是分批建立映射的,当第二层的数组记满后才会开辟下一个
    • 当二层最坏的情况与一层一样大,都是2M大小,(一层2^5 * 2^14 = 2^19,二层是指针数组,32位占4字节,最后还是2M)
    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; // 第一层对应页号的前5个比特位
    	static const int ROOT_LENGTH = 1 << ROOT_BITS; // 第一层存储元素的个数 (32)
    
    	static const int LEAF_BITS = BITS - ROOT_BITS;// 第二层对应页号的其余比特位
    	static const int LEAF_LENGTH = 1 << LEAF_BITS;// 第二层存储元素的个数
    
    
    
  • 代码部分

  1. 增加一个头文件,存放基数树的对象(这里主要用到get与set两个函数,就不对函数相关功能介绍了)
#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() {
	}
};
  1. 在page头文件中,创建基数树的成员变量,替换掉原来的map

    //std::unordered_map<PAGE_ID, Span*> _idSpanMap;
    TCMalloc_PageMap1<32 - PAGE_SHIFT> _idSpanMap;
    
  2. 更改page.cpp中的函数

      1. Span* PageCache::NewSpan(size_t k)

        把原先的[]替换为get
        //_idSpanMap[span->_pageId] = span;
        _idSpanMap.set(span->_pageId, span);
        
      1. Span* PageCache::MapObjectToSpan(void obj)

        Span* PageCache::MapObjectToSpan(void* obj)
        {
        	PAGE_ID id = ((PAGE_ID)obj >> PAGE_SHIFT);
        	auto ret = (Span*)_idSpanMap.get(id);
        	assert(ret != nullptr);
        	return ret;
        }
        

        ​ 这里不用在加锁了,因为基数树的结构它是固定的,而unordered_map在插入时可能会超过负载因子,导致结构变化所以要加锁

      1. void** PageCache::ReleaseSpanToPageCache(Span* span)

        //auto ret = _idSpanMap.find(prevId);
        //if (ret == _idSpanMap.end())
        //	break;
        
        auto ret = (Span*)_idSpanMap.get(prevId);
        if (ret == nullptr)
            break;
        
        //Span* prevSpan = ret->second;
        Span* prevSpan = ret;
        if (prevSpan->_isUse == true)
        
            
        //_idSpanMap[span->_pageId] = span;
        //_idSpanMap[span->_pageId+span->_n-1] = span;
        _idSpanMap.set(span->_pageId, span);
        _idSpanMap.set(span->_pageId + span->_n - 1, span);
        

优化后的运行结果如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K01aFHT3-1676831404640)(G:\Typora\图片保存\image-20230217220439167.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QWjQPwoV-1676831404641)(G:\Typora\图片保存\image-20230217220516641.png)]

​ thread cache使用TLS解决多线程下争锁的问题,且对每个线程都提供了一个缓存(内存池),这也是内存池高效的地方

​ central cache可以调节单个线程的thread cache占用过多内存,使所有线程都能均衡使用内存,到达“负载均衡”;并且它是用于碎内存回收的桥梁,跨度在page与thread之间,由thread的碎内存合并到central中,再由central释放给page就快很多了。

​ page cache 哈希桶中挂载的Span,记录了内存(页)的属性,方便内存回收合并工作,碎块内存可以通过Span中的属性合并成完整的页,而页又可以用于分配,有效缓解内存碎片问题。

本项目主要用于学习,有误地方还请大佬们指点

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值