C++实现的高性能内存池项目

C++实现的高性能内存池项目

一、项目介绍

复制代码
  这个项目是做什么的?当前项⽬是实现⼀个⾼并发的内存池,他的原型是google的⼀个开源项⽬tcmalloc,    
tcmalloc全称Thread-Caching Malloc,即线程缓存的malloc,实现了⾼效的多线程内存管理,⽤于替代系统  
的内存分配相关的函数(malloc、free)。我们这次实现的这个项目就是抽取出谷歌的tcmalloc中比较重要的分层  
设计的思想,实现多线程环境下比C++原生库中的malloc函数更快的一个函数。这就是我们这个项目整体的目标,

二、内存池

1、池化技术

复制代码
  所谓"池化技术",就是程序先向系统申请过量的资源,然后⾃⼰管理,以备不时之需。之所以要申请过量的资源,是因为  
每次申请该资源都有较⼤的开销,不如提前申请好了,这样使⽤时就会变得⾮常快捷,⼤大提⾼程序运⾏效率。
    在计算机中,有很多使⽤"池"这种技术的地⽅,除了内存池,还有连接池、线程池、对象池等。以服务器上的线程池为例,  
它的主要思想是:先启动若⼲数量的线程,让它们处于睡眠状态,当接收到客⼾端的请求时,唤醒池中某个睡眠的线程,让  
它来处理客⼾端的请求,当处理完这个请求,线程⼜进⼊睡眠状态。

2、内存池

复制代码
  内存池是指程序预先从操作系统申请⼀块⾜够⼤内存,此后,当程序中需要申请内存的时候,不是直接向操作系统申请,⽽是  
直接从内存池中获取;同理,当程序释放内存的时候,并不真正将内存返回给操作系统,⽽是返回内存池。当程序退出(或者特  
定时间)时,内存池才将之前申请的内存真正释放。

3、内存池所要解决的问题

复制代码
  内存池主要解决的当然是效率的问题,其次如果作为系统的内存分配器的⻆度,还需要解决⼀下内存碎⽚的问题。那么什么是  
内存碎⽚呢?再需要补充说明的是内存碎⽚分为外碎⽚和内碎⽚,上⾯我们讲的外碎⽚问题。外部碎⽚是⼀些空闲的连续内存  
区域太⼩,这些内存空间不连续,以⾄于合计的内存⾜够,但是不能满⾜⼀些的内存分配申请需求。内部碎⽚是由于⼀些对⻬的  
需求,导致分配出去的空间中⼀些内存⽆法被利⽤。

三、项目基础-设计一个定长的内存池

复制代码
  代码编写思路:首先创建一个ObjectPoo.h文件,在这个文件中我们创建管理内存池的类ObjectPool。这个内存池是这  
样工作的:如果别人向我申请内存的话,我如果没有内存那么我就向系统申请一块大块的内存,然后我要统计我还剩的内存空  
间,这样下一次别人找我申请的时候我就知道我剩的空间足不足够了;现在别人空间用完以后还要把申请的空间还回来的,然  
后我要管理这些还回来的空间,因为我们这里是定长的内存池,什么是定长的内存池?就是每次申请的空间的长度都是相同的,  
换句话说就是这次他把空间还给我了,下一次别人要申请的时候我就可以把这块换回来的空间给他。至此该类的框架结束,下  
面结合代码一步一步介绍类的具体实现。

ObjectPool.h

c++ 复制代码
//首先包含一些头文件
#include <iostream>
#include <new>
#include <vector>
//为了不让命名污染,因此这里我们不使用using namespace std,而是只包一下常用的std::cout,std::cin
using std::cout;
using std::cin;
//使用类型模版参数,使得该定长内存池能够获取不同类型的对象
template <class T>
//定义我们的最重要的类,管理内存池
class ObjectPool
{
private:
	//想一想我们实现的思路中做了什么?
	//首先如果我们没有内存的话需要向堆要,那么我们是不是应该用一个指针来指向我们申请的内存?
	//况且当我们给出内存的时候也需要指针来指向剩下的内存空间,这里设计指针的更新操作
	//指针只是一个数字,但是我们申请多少字节取决于T,这里我们用char*来表示指针的类型,因为char*的指针在  
	++的时候每次只移动一个字节,随着申请空间的不同我们只需要让指针移动是sizeof(T)即可
	char* memory = nullptr;
	//再想一下?我们是不是要记录现在所剩的空间的大小?如果不够的话就要向堆申请了
	size_t _remainBytes = 0;
	//然后再想一下?别人申请空间使用完之后是要还给我的,因此需要管理这些空间,下一次别人再申请的时候我如果说  
	//有这些空间我是可以直接给出去的,不用再去切割了
	void* _freeList = nullptr;
	//好,到这里我们先写这么多私有成员,在写公有接口的时候如果发现有需要添加新的成员我们再加
public:
	//提供new的接口,实现申请一块空间并返回空间的首字节的地址
	T* New()
	{
		//先思考一下我们该怎么写这个函数?
		//要申请的字节数是sizeof(T),如果之前有换回来的内存空间,也就是_freeList挂着的有内存,我们就可以把  
		//第一块内存给出去
		//但是如果_freeList为空的话我们就要看_remainBytes是否有足够的空间,如果说有那好我们从memory开始  
		//切出sizeof(T)大小的字节数给出去,但是如果没有我们就要向堆区申请一块大空间,然后再切割。
		//逻辑已经出来了,现在我们来写代码
		T* obj = nullptr;
		if(_freeList) //有还回来的内存
		{
			T* next = *(void**)_freeList;
			obj = (T*)_freeList;
			_freeList = next;
		}
		//没有换回来的内存
		else
		{
			//剩余的空间不够的话
			if(_remainBytes < sizeof(T))
			{
				_remainBytes = 128*1024;
				//直接申请128页
				_memory = (char*)malloc(_remainBytes);
				if(_memory == NULL)
				{
					//抛异常
					throw std::bad_alloc();
				}
			}
			//现在空间都是处于足够的状态
			obj = (T*)_memory;  //先将指针按在这
			//因为我们开的空间是要能存储一个指针大小的,因此如果不够的话需要扩展到一个指针大小
			size_t objSize = sizeof(T)>sizeof(void*) ? sizeof(T) : sizeof(T);
			_memory += objSize;
			_remainBytes -= objSize;
		}
		//C++在已分配的内存上构建对象的一种方式。
		new(obj)T;  //new(指针)类
		return obj;
	}
	//写一个用来释放空间的函数
	//是别人用完空间了,然后把空间还给我,我要将这块空间挂到_freeList上面管理起来
	void Delete(T* obj)
	{
		//调用析构释放这块空间
		obj->~T();
		//将这块空间挂载
		*(void**)obj = _freeList;
		_freeList = obj;
	}
};

四、项目架构

复制代码
  concurrent memory pool主要由以下3个部分构成:
1. thread cache:线程缓存是每个线程独有的,⽤于⼩于256KB的内存的分配,线程从这⾥申请内存不需要加锁,每个线程  
独享⼀个cache,这也就是这个并发线程池⾼效的地⽅。
2.central cache:中⼼缓存是所有线程所共享,thread cache是按需从central cache中获取的对象。  
central cache合适的时机回收thread cache中的对象,避免⼀个线程占⽤了太多的内存,⽽其他线程的内存吃紧,达到内  
存分配在多个线程中更均衡的按需调度的⽬的。central cache是存在竞争的,所以从这⾥取内存对象是需要加锁,⾸先这⾥  
⽤的是桶锁,其次只有thread cache的没有内存对象时才会找central cache,所以这⾥竞争不会很激烈。
3.page cache:⻚缓存是在central cache缓存上⾯的⼀层缓存,存储的内存是以⻚为单位存储及分配的,central   
cache没有内存对象时,从page cache分配出⼀定数量的page,并切割成定⻓⼤⼩的⼩块内存,分配给central cache。  
当⼀个span的⼏个跨度⻚的对象都回收以后,page cache会回收central cache满⾜条件的span对象,并且合并相邻的⻚,  
组成更⼤的⻚,缓解内存碎⽚的问题。

第一层:⾼并发内存池--thread cache

复制代码
   thread cache是哈希桶结构,每个桶是⼀个按桶位置映射⼤⼩的内存块对象的⾃由链表。每个线程都会有  
 ⼀个thread cache对象,这样每个线程在这⾥获取对象和释放对象时是⽆锁的。

申请内存:

  1. 当内存申请size<=256KB,先获取到线程本地存储的thread cache对象,计算size映射的哈希桶⾃由链表下标i。
  2. 如果⾃由链表_freeLists[i]中有对象,则直接Pop⼀个内存对象返回。
  3. 如果_freeLists[i]中没有对象时,则批量从central cache中获取⼀定数量的对象,插⼊到⾃由链表并返回⼀个对象。
    释放内存:
  4. 当释放内存⼩于256k时将内存释放回thread cache,计算size映射⾃由链表桶位置i,将对象Push到_freeLists[i]。
  5. 当链表的⻓度过⻓,则回收⼀部分内存对象到central cache。

我们创建这样几个文件:common.h这个头文件用来项目中公共的函数和宏,ThreadCache.cpp这个源文件用来写第一层thread cahce,ThreadCache.h这个是对应的头文件。下面我们写它们的代码实现。

ThreadCache.h

c++ 复制代码
#pragma once
#include "Common.h"  //包含公有部分头文件
//声明ThreadCache类
class ThreadCache
{
public:
	//要申请的空间大小为size,返回指向空间的指针
	void* Allocate(size_t size);
	//给定空间和空间的大小,释放空间
	//注意,这里我们释放空间不是说就不管这块空间了,相反我们还是要将这块空间给管理起来
	void Deallocate(void* ptr,size_t size);
	//当ThreadCahce这一层的缓存空间不足够时就要向下一层central cache申请空间
	//传入的参数是数组的索引以及空间对齐后的大小
	void* FetchFromCentralCache(size_t index,size_t size);
	//释放空间时发现链表过长,则回收内存到中心缓存central cache
	void ListTooLong(FreeList& list,size_t size);
private:
	FreeList _freeLists[NFREELIST];  //208条链表
};
//注意:这里是用TLS技术实现线程相互独立数据
static _declspec(thread) ThreadCache* pTLSThreadCahce = nullptr;

ok,我们接下来来写Common.h,在这个头文件中我们要实现内存对齐,也就是要建立一套映射规则。首先,我们知道,用户申请的空间可能是3字节,4字节,5字节等等,如果说每一个字节我们都要去建立一个链表把它们挂起来的话那么是不是要建立的链表的数量就太多了?因此我们想了一个办法说能不能就是3字节4字节我都给它们提升一下我给8字节,然后15字节我给16字节,也就是说对于一些要申请的内存我多开一点空间。这样做的好处是什么?我现在不需要创建那么多的链表了,我只需要创建挂接8字节的,挂接16字节的等等就可以了,这就是我们要建立的一套内存映射规则。在Common.h中实现。这一套规则如下:

申请[1,128]字节的话使用8字节对齐,则数组的下标为[0,16),即是从0到15。

c++ 复制代码
#pragma once
#include <iostream>

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

//映射规则
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)
	{
		//该函数将给定的字节数bytes向上对齐到alignNum的整数倍
		//
		return ((bytes + alignNum - 1) & ~(alignNum - 1));
	}
	//
	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
		{
			//即2^13=8192字节对齐。和上面是一样的
			// 巨型请求:对齐到系统页(PAGE_SHIFT=13 → 8KB)
			return _RoundUp(size, 1 << PAGE_SHIFT);
		}
	}
	//                                    //align_shift 应该是 3,因为 2^3=8
	static inline size_t _Index(size_t bytes, size_t align_shift)
	{
		//其本质是向上取整到对齐单元的整数倍,再转换为从0开始的连续索引。
		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[0] + group_array[1] + group_array[2];
		}
		else if (bytes <= 256 * 1024)
		{
			//第五个区间的桶数量由_Index的结果范围决定。
			return _Index(bytes-64*1024,13)+ group_array[3] + group_array[2] + group_array[1] + group_array[0];
		}
		else {
			assert(false);
		}
		return -1; //错误
	}

	//一次thread cahce 从中心缓存获取多少个
	static size_t NumMoveSize(size_t size)
	{
		assert(size > 0);
		//小的给多点,大的给小点
		int num = MAX_BYTES / size;
		if (num < 2)
			num = 2;
		if (num > 512)
			num = 512;
		return num;
		
	}

	//一次向系统获取几个页
	static size_t NumMovePage(size_t size)
	{
		//先计算向central cache获取多少个
		size_t num = NumMoveSize(size);
		//再计算central cache应向page cache获取的页数
		//计算总字节数
		size_t npage = num * size;
		npage >>= PAGE_SHIFT;  //除以8k
		if (npage == 0)
			npage = 1;
		return npage;
	}

};

第二层 - central cache

central cache也是⼀个哈希桶结构,他的哈希桶的映射关系跟thread cache是⼀样的。不同的是他的每个哈希桶位置挂是SpanList链表结构,不过每个映射桶下⾯的span中的⼤内存块被按映射关系切成了⼀个个⼩内存块对象挂在span的⾃由链表中。

申请内存:

  1. 当thread cache中没有内存时,就会批量向central cache申请⼀些内存对象,这⾥的批量获取对象的数量使⽤了类似⽹络tcp协议拥塞控制的慢开始算法;central cache也有⼀个哈希映射的spanlist,spanlist中挂着span,从span中取出对象给thread cache,这个过程是需要加锁的,不过这⾥使⽤的是⼀个桶锁,尽可能提⾼效率。
  2. central cache映射的spanlist中所有span的都没有内存以后,则需要向page cache申请⼀个新的
    span对象,拿到span以后将span管理的内存按⼤⼩切好作为⾃由链表链接到⼀起。然后从span中取对象给thread cache。
  3. central cache的中挂的span中use_count记录分配了多少个对象出去,分配⼀个对象给thread
    cache,就++use_count
    释放内存:
  4. 当thread_cache过⻓或者线程销毁,则会将内存释放回central cache中的,释放回来时--
    use_count。当use_count减到0时则表⽰所有对象都回到了span,则将span释放回pagecache,page cache中会对前后相邻的空闲⻚进⾏合并。

Central cache 代码框架

cpp 复制代码
#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);
   //将一定数量的对象释放到span
   void ReleaseListToSpans(void* start, size_t byte_size);
private:
   SpanList _spanLists[NFREELIST];
private:
   //将构造函数设置为私有,以至于程序中只有一个单例
   CentralCache()
   { }
   //禁用拷贝构造
   CentralCache(const CentralCache&) = delete;
   static CentralCache _sInst;  //唯一的一个对象
};
cpp 复制代码
#include "CentralCache.h"
#include "PageCache.h"
CentralCache CentralCache::_sInst;

Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{
	// 这段代码遍历传入的span链表,查找有没有空闲对象可分配的span。
	// 如果找到有可用空闲对象的span(即_freeList不为空),就立即返回该span指针。
	// 如果整条链表都没有可用的span,说明central cache没有可分配对象,此时需要解锁互斥锁,
	// 以便其他线程能够访问或修改该链表。
	Span* it = list.Begin();
	while (it != list.End())
	{
		// 找到有空闲对象的span就返回
		if (it->_freeList != nullptr)
		{
			return it;
		}
		else
		{
			it = it->_next;
		}
	}
	// 链表里没有可用span,解锁互斥锁
	list._mtx.unlock();

	PageCache::GetInstance()->_pageMtx.lock();
	//_sInst._pageMtx.lock();
	Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));
	span->_isUse = true;
	span->_objSize = size;
	//
	PageCache::GetInstance()->_pageMtx.unlock();
	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 = start;
		start += size;
	}
	NextObj(tail) = nullptr;

	list._mtx.lock();
	list.PushFront(span);

	return span;
}

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(span->_freeList);

	start = span->_freeList;
	end = start;
	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;

	int j = 0;
	void* cur = start;
	while (cur)
	{
		cur = NextObj(cur);
		++j;
	}

	if (j != actualNum)
	{
		int x = 0;
	}
	_spanLists[index]._mtx.unlock();
	return actualNum;
}

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--;
	
		if (span->_useCount == 0)
		{
			_spanLists[index].Erase(span);
		
			span->_freeList = nullptr;
			span->_next = nullptr;
			span->_prev = nullptr;

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

第三层 - page cache

申请内存:

  1. 当central cache向page cache申请内存时,page cache先检查对应位置有没有span,如果没有则向更⼤⻚寻找⼀个span,如果找到则分裂成两个。⽐如:申请的是4⻚page,4⻚page后⾯没有挂span,则向后⾯寻找更⼤的span,假设在10⻚page位置找到⼀个span,则将10⻚page span分裂为⼀个4⻚page span和⼀个6⻚page span。
  2. 如果找到_spanList[128]都没有合适的span,则向系统使⽤mmap、brk或者是VirtualAlloc等⽅式申请128⻚page span挂在⾃由链表中,再重复1中的过程。
  3. 需要注意的是central cache和page cache 的核⼼结构都是spanlist的哈希桶,但是他们是有本质区别的,central cache中哈希桶,是按跟thread cache⼀样的⼤⼩对⻬关系映射的,他的spanlist中挂的span中的内存都被按映射关系切好链接成⼩块内存的⾃由链表。⽽page cache 中的spanlist则是按下标桶号映射的,也就是说第i号桶中挂的span都是i⻚内存。
    释放内存:
  4. 如果central cache释放回⼀个span,则依次寻找span的前后page id的没有在使⽤的空闲span,看是否可以合并,如果合并继续向前寻找。这样就可以将切⼩的内存合并收缩成⼤的span,减少内存碎⽚。
    Page cache 代码框架:
cpp 复制代码
// 1.page cache是⼀个以⻚为单位的span⾃由链表 
// 2.为了保证全局只有唯⼀的page cache,这个类被设计成了单例模式。 
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;
 TCMalloc_PageMap1<32 - PAGE_SHIFT> _idSpanMap;
 PageCache()
 {}
 PageCache(const PageCache&) = delete;
 static PageCache _sInst;
};
cpp 复制代码
#include "PageCache.h"

// 这里为什么能调用?不应该通过GetInstance函数进行调用吗???
PageCache PageCache::_sInst;

Span* PageCache::NewSpan(size_t k)
{
	//获取一个k页的span
	assert(k > 0);
	//
	if (k > NPAGES - 1)
	{
		//申请k页
		//只有要切分的页才要每一个页号都映射到span
		void* ptr = SystemAlloc(k);
		Span* span = _spanPool.New();

		//页号连续?两个SystemAlloc中
		//页的起始号
		span->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
		//span管理多少页
		span->_n = k;
		/*_idSpanMap[span->_pageId] = span;*/
		_idSpanMap.set(span->_pageId, span);
		

		return span;
	}

	//第一步 先检查第k个桶里面有没有span
	if (!_spanLists[k].Empty())
	{
		//
		Span* kSpan = _spanLists[k].PopFront();
		//建立id和span的映射,
		//每个页号都能找到这个span
		for (PAGE_ID i = 0; i < kSpan->_n; i++)
		{
			/*_idSpanMap[kSpan->_pageId + i] = kSpan;*/
			_idSpanMap.set(kSpan->_pageId + i, kSpan);
		}
		
		return kSpan;
	}
	//查找后面
	for (size_t i = k + 1; i < NPAGES; i++)
	{
		if (!_spanLists[i].Empty())
		{
			//不为空
			Span* nSpan = _spanLists[i].PopFront();
			//切割
			Span* kSpan = new Span;
			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;*/
			_idSpanMap.set(nSpan->_pageId, nSpan);
			_idSpanMap.set(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;*/
				_idSpanMap.set(kSpan->_pageId + i, kSpan);
			}
			
			return kSpan;
		}
	}
	// 走到这个位置就说明后面没有大页的span了
	// 这时就去找堆要一个128页的span
	Span* bigSpan = _spanPool.New();
	//向堆申请128页的内存,并返回起始地址给ptr
	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)
Span* PageCache::MapObjectToSpan(void* obj)
{
	//地址得到所在的页号
	PAGE_ID id = ((PAGE_ID)obj >> PAGE_SHIFT);
	//
	auto ret = _idSpanMap.get(id);
	assert(ret != nullptr);
	return (Span*)ret;
}

//一遍又一遍
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)
	{
		//span上一个span的结尾的页号
		PAGE_ID prevID = span->_pageId - 1;
		/*auto ret = _idSpanMap.find(prevID);
		if (ret == _idSpanMap.end())
		{
			break;
		}*/
		auto ret = (Span*)_idSpanMap.get(prevID);
		if (ret == nullptr)
		{
			break;
		}
		//获取上一个span

		Span* prevSpan = ret;
		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);
		//释放描述结构
		_spanPool.Delete(prevSpan);
	}
	//向后合并
	while (1)
	{
		PAGE_ID nextId = span->_pageId + span->_n;
		/*auto ret = _idSpanMap.find(nextId);
		if (ret == _idSpanMap.end())
		{
			break;
		}*/
		auto ret = (Span*)_idSpanMap.get(nextId);
		if (ret == nullptr)
		{
			break;
		}
		Span* nextSpan = ret;
		if (nextSpan->_isUse == true)
		{
			break;
		}
		if (nextSpan->_n + span->_n > NPAGES - 1)
		{
			break;
		}
		span->_n += nextSpan->_n;
		_spanLists[nextSpan->_n].Erase(nextSpan);
		_spanPool.Delete(nextSpan);
	}
	//插入合并完成之后的span
	_spanLists[span->_n].PushFront(span);
	//本来?
	span->_isUse = false;

	/*_idSpanMap[span->_pageId] = span;
	_idSpanMap[span->_pageId + span->_n - 1] = span;*/
	_idSpanMap.set(span->_pageId, span);
	_idSpanMap.set(span->_pageId + span->_n - 1, span);
}

基数树优化

cpp 复制代码
#pragma once
//针对性能瓶颈采取基数数进行优化
#include "Common.h"

//语法:非类型模版参数
template <int BITS>
class TCMalloc_PageMap1 {
private:
	//2的BITS次方
	static const int LENGTH = 1 << BITS;
	void** array_;
public:
	//unsigned int
	typedef uintptr_t Number;

	//explicit防止编译器进行隐式类型转换
	//explicit防止编译器进行隐式类型转换
	explicit TCMalloc_PageMap1()
	{
		//计算数组的总字节数
		size_t size = sizeof(void*) << BITS;
		//将size对齐到页大小的倍数
		size_t alignSize = SizeClass::_RoundUp(size, 1 << PAGE_SHIFT);
		//
		array_ = (void**)SystemAlloc(alignSize >> PAGE_SHIFT);
		memset(array_, 0, sizeof(void*) << BITS);
	}
	//越界的含义
	void* get(Number k) const
	{
		if ((k >> BITS) > 0)
		{
			return NULL;
		}
		return array_[k];
	}
	//
	void set(Number k, void* v)
	{
		//页号映射span*
		array_[k] = v;
	}
};
//非类型模版参数
template <int BITS>
class TCMalloc_PageMap2 {
private:
	//根节点固定占用5位
	static const int ROOT_BITS = 5;
	//32个根节点
	static const int ROOT_LENGTH = 1 << ROOT_BITS;

	//叶子结点位数
	static const int LEAF_BITS = BITS - ROOT_BITS;
	//叶子结点的容量
	static const int LEAF_LENGTH = 1 << LEAF_BITS;
	//叶子结点的结构
	struct Leaf {
		void* values[LEAF_LENGTH];
	};
	//第一层的结构
	Leaf* root_[ROOT_LENGTH];
	//
	/*void* (*allocator_)(size_t);*/
public:
	typedef uintptr_t Number;

	//explicit禁止隐式拷贝,只能直接构造
	explicit TCMalloc_PageMap2()
	{
		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;
	}
	
	//遍历从start到start+n-1的键,
	//为每个根节点索引分配叶子结点
	//???
	bool Ensure(Number start, size_t n)
	{
		for (Number key = start; key <= start + n - 1;)
		{
			const Number i1 = key >> LEAF_BITS;

			if (i1 >= ROOT_LENGTH)
				return false;
			if (root_[i1] == NULL)
			{
				static ObjectPool<Leaf> leafPool;
				Leaf* leaf = (leaf*)leafPool.New();
				memset(leaf, 0, sizeof(*leaf));
				root_[i1] = leaf;
			}
			
			key = ((key >> LEAF_BITS) + 1) << LEAF_BITS;
		}
		return true;
	}
	void PreallocateMoreMemory()
	{
		Ensure(0, 1 << BITS);
	}
};



//三层的基数数
//非类型模版参数
template <int BITS>
class TCMalloc_PageMap3
{
private:
	static const int INTERIOR_BITS = (BITS + 2) / 3;
	static const int INTERIOR_LENGTH = 1 << INTERIOR_BITS;

	//叶节点为位数
	static const int LEAF_BITS = BITS - 2 * INTERIOR_BITS;
	static const int LEAF_LENGTH = 1 << LEAF_BITS;

	//
	struct Node
	{
		Node* ptrs[INTERIOR_LENGTH];
	};
	//
	struct Leaf
	{
		void* values[LEAF_LENGTH];
	};

	Node* root_;
	void* (*allocator_)(size_t);

	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;
	//传入malloc之类的
	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);

			if (i1 >= INTERIOR_LENGTH || i2 >= INTERIOR_LENGTH)
				return false;
			if (root_->ptrs[i1] == NULL)
			{
				Node* n = NewNode();
				if (n == NULL)
					return false;
				root_->ptrs[i1] = n;
			}
			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);
			}
			//?
			//以叶子节点覆盖范围为步长(非单个元素)
			key = ((key >> LEAF_BITS) + 1) << LEAF_BITS;
		}

	}
	void PreallocateMoreMemory() {
	}
};

这个基数树的代码的话是直接从tcmalloc中截取出来的。

五、性能测试

测试文件:Benchmark.cpp

cpp 复制代码
#include "ConcurrentAlloc.h"

void BenchmarkMalloc(size_t ntimes, size_t nworks, size_t rounds)
{
	//nworks应该是几个线程
	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++)
	{
		//lambda
		//[&,k]是捕获列表
		//&表示以引用方式捕获所有外部变量
		//k表示按值方式单独捕获变量k
		vthread[k] = std::thread([&,k]() {
			//匿名函数的函数体
			std::vector<void*> v;
			//预分配ntimes个指针的空间
			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));
				}
				size_t end1 = clock();

				//释放计时
				size_t begin2 = clock();
				for (size_t i = 0; i < ntimes; i++)
				{
					free(v[i]);
				}
				size_t end2 = clock();
				//it seems to really refine your soul
				//v.clear()清空vector,为下一轮测试准备
				// (此时内存已释放,vector仅释放指针存储空间)。
				v.clear();

				malloc_costtime += (end1 - begin1);
				free_costtime += (end2 - begin2);
			}
		});
	}
	//what if there is not &
	for (auto& t : vthread)
	{
		t.join();
	}

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

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

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

void BenchmarkConcurrentMalloc(size_t ntimes, size_t nworks, size_t rounds)
{
	std::vector<std::thread> vthread(nworks);
	std::atomic<size_t> concurrent_malloc_costtime = 0;
	std::atomic<size_t> free_costtime = 0;

	for (size_t k = 0; k < nworks; k++)
	{
		//&表示外面的值以引用的形式看到
		//k表示k这个变量是以数值(拷贝)的形式看到
		vthread[k] = std::thread([&,k]() {
			//线程要执行的函数
			//用来管理申请的空间
			std::vector<void*> v; //对命名还是要更自信一点
			v.reserve(ntimes);
			for (size_t i = 0; i < rounds; i++)
			{
				size_t begin1 = clock();
				for (size_t j = 0; j < ntimes; j++)
				{
					//申请空间,从头开始
					v.push_back(ConcurrentAlloc(16));

				}
				size_t end1 = clock();
				/*concurrent_malloc_costtime += (end1 - begin1);
				printf("%u个线程并发执行%u轮次,每轮次concurrent alloc %u次: 花费:%u ms\n",
					nworks, rounds, ntimes, concurrent_malloc_costtime.load());*/
				//说明是释放的时候有问题
				size_t begin2 = clock();
				//释放空间
				for (size_t j = 0; j < ntimes; j++)
				{
	
					ConcurrentFree(v[j]);
				}
				size_t end2 = clock();
				concurrent_malloc_costtime += (end1 - begin1);
				free_costtime += (end2 - begin2);

				//清空v
				v.clear();
			}
		});
	}

	for (auto& i : vthread)
	{
		i.join();
	}

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

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

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

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

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

	return 0;
}

六、总结与拓展

本项目基于Google tcmalloc的设计思想,实现了一个多线程环境下的高性能内存分配器。核心解决了三个问题:

锁竞争激烈:采用三层架构:ThreadCache(无锁) + CentralCache(桶锁) + PageCache(全局锁)

内存碎片:采用PageCache的前后页合并机制 + 中央缓存的对象复用

小对象效率低:采用自由链表管理 + 慢启动批量分配

实际中我们测试了,当前实现的并发内存池⽐malloc/free是更加⾼效的,那么我们能否替换到系统调⽤malloc呢?实际上是可以的。

• 不同平台替换⽅式不同。基于unix的系统上的glibc,使⽤了weak alias的⽅式替换。具体来说是因为这些⼊⼝函数都被定义成了weaksymbols,再加上gcc⽀持 alias attribute,所以替换就变成了这种通⽤形式:

void* malloc(size_t size) THROW attribute__ ((alias (tc_malloc)))

因此所有malloc的调⽤都跳转到了tc_malloc的实现

相关推荐
ℳ๓₯㎕.空城旧梦1 小时前
C++中的解释器模式
开发语言·c++·算法
想七想八不如114081 小时前
面向对象程序设计--模拟题2查漏补缺
c++·考研
不想写代码的星星1 小时前
C++的'大自然搬运工':一文讲透using的所有用法
c++
2401_879503412 小时前
C++与FPGA协同设计
开发语言·c++·算法
今儿敲了吗3 小时前
46| FBI树
数据结构·c++·笔记·学习·算法
oem1103 小时前
C++中的访问者模式变体
开发语言·c++·算法
暮冬-  Gentle°3 小时前
C++中的工厂方法模式
开发语言·c++·算法
dfafadfadfafa4 小时前
嵌入式C++安全编码
开发语言·c++·算法
计算机安禾4 小时前
【C语言程序设计】第34篇:文件的概念与文件指针
c语言·开发语言·数据结构·c++·算法·visual studio code·visual studio