项目1 | LTCMalloc

在这里插入图片描述
啊我摔倒了..有没有人扶我起来学习....


👱个人主页: 《 C G o d 的个人主页》 \color{Darkorange}{《CGod的个人主页》} CGod的个人主页》交个朋友叭~
💒个人社区: 《编程成神技术交流社区》 \color{Darkorange}{《编程成神技术交流社区》} 《编程成神技术交流社区》加入我们,一起高效学习,收割好Offer叭~
🌱刷题链接: 《 L e e t C o d e 》 \color{Darkorange}{《LeetCode》} LeetCode快速成长的渠道哦~



前言

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

一、项目介绍

1.1 这个项目做的是什么?

  • 当前项目是实现一个多线程环境下的内存池,他的原型是google的一个开源项目tcmalloc,tcmalloc全称Thread-Caching Malloc,即线程缓存的malloc,实现了高效的多线程内存管理,用于替代系统的内存分配相关的函数(malloc、free)
  • 我们这个项目是把tcmalloc最核心的框架简化后拿出来,模拟实现出一个自己的高并发内存池,目的就是学习tcmalloc的精华

1.2 这个项目要求的知识储备

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

二、什么是内存池

2.1 池化技术

  • 所谓“池化技术”,就是程序先向系统申请过量的资源,然后自己管理,以备不时之需。之所以要申请过量的资源,是因为每次申请该资源都有较大的开销,不如提前申请好了,这样使用时就会变得非常快捷,大大提高程序运行效率
  • 在计算机中,有很多使用这种技术的地方,除了内存池,还有连接池、线程池、对象池等。以服务器上的线程池为例,它的主要思想是:先启动若干数量的线程,让它们处于睡眠状态,当接收到客户端的请求时,唤醒池中某个睡眠的线程,让它来处理客户端的请求,当处理完这个请求,线程又进入睡眠状态

2.2 内存池

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

2.3 内存池主要解决的问题

  • 内存池主要解决的当然是效率的问题,其次如果作为系统的内存分配器的角度,还需要解决一下内存碎片的问题。那么什么是内存碎片呢?
    在这里插入图片描述
    再需要补充说明的是内存碎片分为外碎片内碎片,上面我们讲的是外碎片问题。外部碎片是一些空闲的连续内存区域太小,这些内存空间不连续,以至于合计的内存足够,但是不能满足一些的内存分配申请需求。内部碎片是由于一些对齐的需求,导致分配出去的空间中一些内存无法被利用

2.4 malloc

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

三、项目开始

3.1 先设计一个定长的内存池(对象池)

  • 作为程序员(C/C++)我们知道申请内存使用的是malloc,malloc其实就是一个通用的大众货,什么场景下都可以用,但是什么场景下都可以用就意味着什么场景下都不会有很高的性能
  • 下面我们就先来设计一个定长内存池(对象池),当然这个定长内存池在我们后面的高并发内存池中也是有价值的,所以学习他目的有两层:
    • 先熟悉一下简单内存池是如何控制的
    • 作为我们后面内存池的一个基础组件
      在这里插入图片描述
  • _memory指向从堆申请的大块内存,随后切分给每个申请的对象使用;当归还内存的时候,并不是直接还给堆,而是先还给内存池,用_freelist链接管理起来,重复利用
// 直接去堆上按页申请空间
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;
}

template<class T>
class ObjectPool
{
public:
	T* New()
	{
		T* obj = nullptr;
		// 如果自由链表有对象,直接取一个
		if (_freeList)
		{
			void* next = *((void**)_freeList);
			obj = (T*)_freeList;
			_freeList = next;
		}
		else
		{
			// 剩余内存不够一个对象大小时,则重新开大块空间
			if (_remainBytes < sizeof(T))
			{
				_remainBytes = 128 * 1024;
				//_memory = (char*)malloc(_remainBytes);
				_memory = (char*)SystemAlloc(_remainBytes >> 13);
				if (_memory == nullptr) throw std::bad_alloc();
			}
			obj = (T*)_memory;
			size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T); // 如果申请的内存小于一个指针的大小,那就分配一个指针的大小
			_memory += objSize;
			_remainBytes -= objSize;
		}
		// 使用定位new调用T的构造函数初始化
		new(obj)T;
		return obj;
	}
	void Delete(T* obj)
	{
		// 显示调用的T的析构函数进行清理
		obj->~T();
		// 头插到freeList
		*(void**)obj = _freeList;
		_freeList = obj;
	}
private:
	char* _memory = nullptr;   // 指向内存块的指针
	int _remainBytes = 0;      // 内存块中剩余字节数
	void* _freeList = nullptr; // 管理还回来的内存对象的自由链表
};

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

  • 现代很多的开发环境都是多核多线程,在申请内存的场景下,必然存在激烈的锁竞争问题。malloc本身其实已经很优秀,那么我们项目的原型tcmalloc就是在多线程高并发的场景下更胜一筹,所以这次我们实现的内存池需要考虑以下几方面的问题
    1. 性能问题
    2. 多线程环境下,锁竞争问题
    3. 内存碎片问题
  • 高并发内存池主要由以下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对象,并且合并相邻的页,组成更大的页,缓解内存碎片的问题
      在这里插入图片描述

3.3 申请内存的逻辑

  • 为了更好地理解该项目过程,先走一遍申请内存的逻辑,再走释放内存的逻辑

3.3.1 thread cache的申请逻辑

  • thread cache是哈希桶结构,每个桶是一个按桶位置映射大小的内存块对象的自由链表。每个线程都会有一个thread cache对象,这样每个线程在这里获取对象和释放对象时是无锁的(利用TLS)
    在这里插入图片描述

  • 申请内存:

    1. 当内存申请size<=256KB,先获取到线程本地存储的thread cache对象,计算size映射的哈希桶自由链表下标i
    2. 如果自由链表_freeLists[i]中有对象,则直接pop一个内存对象返回
    3. 如果_freeLists[i]中没有对象时,则批量从central cache中获取一定数量的对象,插入到自由链表并返回一个对象
  • TLS——thread local storage:
    关于TLS可以参考一下这篇文章《Thread Local Storage(线程局部存储)TLS》

  • thread cache申请逻辑代码框架:

  1. ThreadCache.h
class ThreadCache
{
public:
	void* allocate(size_t bytes); // 申请内存对象
	void* fetchFromCentralCache(size_t index, size_t bytes); // 从中心缓存获取对象
private:
	FreeList _freeLists[NFREELIST];
};

// TLS thread local storage 线程局部存储的声明
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;
  1. ThreadCache.cpp
void* ThreadCache::allocate(size_t bytes)
{
	assert(bytes <= MAX_BYTES);

	size_t align = SizeClass::roundUp(bytes);
	size_t index = SizeClass::index(bytes);

	if (!_freeLists[index].empty())
	{
		// thread cache对应桶里还有内存,直接取
		return _freeLists[index].pop();
	}
	else
	{
		// thread cache对应桶里没有内存了,要去central cache那里取
		return fetchFromCentralCache(index, align);
	}
}
void* ThreadCache::fetchFromCentralCache(size_t index, size_t bytes)
{
	// 慢开始反馈调节算法
	// 1、最开始不会一次向central cache一次批量要太多,因为要太多了可能用不完
	// 2、如果你不断申请这个bytes大小内存需求,那么batchNum就会不断增长,直到上限
	// 3、bytes越大,一次向central cache要的batchNum就越小
	// 4、bytes越小,一次向central cache要的batchNum就越大
	size_t batchNum = min(SizeClass::NumMoveSize(bytes), _freeLists[index].maxSize());
	if (batchNum == _freeLists[index].maxSize()) _freeLists[index].maxSize() += 1;

	void* start = nullptr;
	void* end = nullptr;
	size_t actualNum = CentralCache::getInstance()->fetchRangeObj(start, end, batchNum, bytes);
	assert(actualNum); // 实际拿到的个数

	if (actualNum == 1) // 如果拿到一个,那么直接返回拿来用
	{
		assert(start == end);
		return start;
	}
	else // 如果拿到多个,返回第一个拿来用,其他先插入桶里
	{
		_freeLists[index].pushRange(NextObj(start), end, actualNum - 1);
		return start;
	}
}
  1. 此时Common.h里的方法有
static const size_t MAX_BYTES = 256 * 1024;
static const size_t NFREELIST = 208;

static void*& NextObj(void* obj)
{
	return *(void**)obj;
}
// 管理切分好的自由链表
class FreeList
{
public:
	void push(void* obj)
	{
		assert(obj); // ************
		//头插
		NextObj(obj) = _freeList;
		_freeList = obj;
		++_size;
	}
	void pushRange(void* start, void* end, size_t len)
	{
		NextObj(end) = _freeList;
		_freeList = start;
		_size += len;
	}
	void* pop()
	{
		assert(_freeList); // **************
		//头删
		void* obj = _freeList;
		_freeList = NextObj(obj);
		--_size;

		return obj;
	}
	bool empty()
	{
		return _freeList == nullptr;
	}
	size_t& maxSize()
	{
		return _maxSize;
	}
private:
	void* _freeList = nullptr;
	size_t _maxSize = 1;
	size_t _size = 0;
};

// 计算对象大小的对齐映射规则
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)
	// 
	// 对齐申请的bytes
	static inline size_t __roundUp(size_t bytes, size_t align)
	{
		return ((bytes + align - 1) & ~(align - 1));
	}
	static inline size_t roundUp(size_t bytes)
	{
		if (bytes <= 128)
		{
			return __roundUp(bytes, 8);
		}
		else if (bytes <= 1024)
		{
			return __roundUp(bytes, 16);
		}
		else if (bytes <= 8 * 1024)
		{
			return __roundUp(bytes, 128);
		}
		else if (bytes <= 64 * 1024)
		{
			return __roundUp(bytes, 1024);
		}
		else if (bytes <= 256 * 1024)
		{
			return __roundUp(bytes, 8 * 1024);
		}
		else
		{
			assert(false);
		}
	}
	
	// 计算index
	static inline size_t __index(size_t bytes, size_t align_shift)
	{
		return ((bytes + (1 << align_shift) - 1) >> align_shift) - 1;
	}
	static inline size_t index(size_t bytes)
	{
		assert(bytes <= MAX_BYTES);

		static int group_array[4] = { 16, 56, 56, 56 }; // 每个区间有多少个桶
		if (bytes <= 128)
		{
			return __index(bytes, 3);
		}
		else if (bytes <= 1024)
		{
			return __index(bytes - 128, 4) + group_array[0];
		}
		else if (bytes <= 8 * 1024)
		{
			return __index(bytes - 1024, 7) + group_array[1] + group_array[0];
		}
		else if (bytes <= 64 * 1024)
		{
			return __index(bytes - 8 * 1024, 10) + group_array[2] + group_array[1] + group_array[0];
		}
		else if (bytes <= 256 * 1024)
		{
			return __index(bytes - 64 * 1024, 13) + group_array[3] + group_array[2] + group_array[1] + group_array[0];
		}
		else
		{
			assert(false);
			return -1;
		}
	}
	
	// 一次thread cache从中心缓存获取多少个对象
	static size_t NumMoveSize(size_t bytes)
	{
		assert(bytes);
		// [2, 512],一次批量移动多少个对象的(慢启动)上限值
		// 小对象一次批量上限高
		// 大对象一次批量上限低
		size_t num = MAX_BYTES / bytes;
		if (num < 2) num = 2;
		else if (num > 512) num = 512;

		return num;
	}
};

3.3.2 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记录分配了多少个对象出去,分配一个对象给threadcache,就++use_count
  • CentralCache 申请逻辑代码框架:
  1. CentralCache.h
//单例模式————饿汉模式
class CentralCache
{
public:
	static CentralCache* getInstance()
	{
		return &_Inst;
	}
	Span* GetOneSpan(SpanList& list, size_t bytes); // 获取一个非空的span
	size_t fetchRangeObj(void*& start, void*& end, size_t batchNum, size_t bytes); // 从中心缓存获取一定数量的对象给thread cache
private:
	CentralCache(){}
	CentralCache(const CentralCache&) = delete;

	SpanList _spanLists[NFREELIST];
	static CentralCache _Inst; // 类内声明,类外定义
};
  1. CentralCache.cpp
CentralCache CentralCache::_Inst;

// 获取一个非空的span
Span* CentralCache::GetOneSpan(SpanList& list, size_t bytes)
{
	// 查看当前的spanlist中是否还有非空的span
	Span* it = list.begin();
	while (it != list.end())
	{
		if (it->_freeList != nullptr) return it;
		else it = it->_next;
	}
	list._mtx.unlock(); // 先把central cache的桶锁解掉,这样如果其他线程释放内存对象回来,不会阻塞
	// 走到这里说没有空闲span了,只能找page cache要
	PageCache::getInstance()->_pageMtx.lock();
	Span* span = PageCache::getInstance()->newSpan(SizeClass::NumMovePage(bytes));
	span->_isUse = true;
	span->_objSize = bytes;
	PageCache::getInstance()->_pageMtx.unlock();

	// 对获取span进行切分,不需要加锁,因为这会其他线程访问不到这个span
	// 计算span的大块内存的起始地址和大块内存的大小(字节数)
	char* start = (char*)(span->_pageId << PAGE_SHIFT);
	size_t len = span->_n << PAGE_SHIFT;
	char* end = start + len;

	// 把大块内存切成自由链表链接起来
	// 1、先切一块下来去做头,方便尾插
	span->_freeList = start;
	start += bytes;
	void* tail = span->_freeList;
	while (start < end)
	{
		NextObj(tail) = start;
		tail = NextObj(tail);
		start += bytes;
	}
	NextObj(tail) = nullptr; //****************************
	// 切好span以后,需要把span挂到桶里面去的时候,先加锁
	list._mtx.lock();
	list.pushFront(span);
	return span;
}
// 从中心缓存获取一定数量的对象给thread cache
size_t CentralCache::fetchRangeObj(void*& start, void*& end, size_t batchNum, size_t bytes)
{
	size_t index = SizeClass::index(bytes);
	_spanLists[index]._mtx.lock(); // 加锁
	
	Span* span = GetOneSpan(_spanLists[index], bytes);
	assert(span); // 最起码要拿到一个span
	assert(span->_freeList); // 拿到了的span不能为空

	// 从span中获取batchNum个对象
	// 如果不够batchNum个,有多少拿多少
	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->_useCount += actualNum;
	span->_freeList = NextObj(end);
	NextObj(end) = nullptr;

	_spanLists[index]._mtx.unlock(); // 解锁

	return actualNum;
}

其中,疑难点在于:
在这里插入图片描述在这里插入图片描述

  1. 此时Common.h里的方法增加如下
static const size_t PAGE_SHIFT = 13;

// 一次thread cache从中心缓存获取多少个对象
static size_t NumMoveSize(size_t bytes)
{
	assert(bytes);
	// [2, 512],一次批量移动多少个对象的(慢启动)上限值
	// 小对象一次批量上限高
	// 大对象一次批量上限低
	size_t num = MAX_BYTES / bytes;
	if (num < 2) num = 2;
	else if (num > 512) num = 512;
	return num;
}
static size_t NumMovePage(size_t bytes)
{
	size_t num = NumMoveSize(bytes);
	size_t npage = num * bytes;
	npage >>= PAGE_SHIFT;
	if (npage == 0) npage = 1;
	return npage;
}
// 管理多个连续页大块内存跨度结构
struct Span
{
	PAGE_ID _pageId = 0; // 页号
	size_t _n = 0;       // 页数

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

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

	bool _isUse = false;
};
// 带头双向循环链表 
class SpanList
{
public:
	SpanList()
	{
		_head = new Span;
		_head->_next = _head;
		_head->_prev = _head;
	}
	bool empty()
	{
		return _head->_next == _head;
	}
	void pushFront(Span* span)
	{
		insert(begin(), span);
	}
	Span* popFront()
	{
		Span* front = _head->_next;
		erase(front);
		return front;
	}
	Span* begin()
	{
		return _head->_next;
	}
	Span* end()
	{
		return _head;
	}
	void insert(Span* pos, Span* newSpan)
	{
		assert(pos);
		assert(newSpan);

		Span* prev = pos->_prev;
		prev->_next = newSpan;
		newSpan->_prev = prev;
		newSpan->_next = pos;
		pos->_prev = newSpan;
	}
	void erase(Span* pos)
	{
		assert(pos);
		assert(pos != _head); //***********

		Span* prev = pos->_prev;
		Span* next = pos->_next;

		 1、条件断点
		 2、查看栈帧
		//if (prev == nullptr)
		//{
		//	int x = 0;
		//}

		prev->_next = next;
		next->_prev = prev;
	}
private:
	Span* _head;
public:
	std::mutex _mtx; // 桶锁
};

3.3.3 page cache的申请逻辑

  • thread cache和central cache的映射规则一致,但page cache就大大不同了在这里插入图片描述

  • 申请内存:

    1. 当central cache向page cache申请内存时,page cache先检查对应位置有没有span,如果没有则向更大页寻找一个span,如果找到则分裂成两个。比如:申请的是4页page,4页page后面没有挂span,则向后面寻找更大的span,假设在10页page位置找到一个span,则将10页pagespan分裂为一个4页page span和一个6页page span
    2. 如果找到_spanList[128]都没有合适的span,则向系统使用mmap、brk或者是VirtualAlloc等方式申请128页page span挂在自由链表中,再重复1中的过程
    3. 需要注意的是central cache和page cache 的核心结构都是spanlist的哈希桶,但是他们是有本质区别的,central cache中哈希桶,是按跟thread cache一样的大小对齐关系映射的,他的spanlist中挂的span中的内存都被按映射关系切好链接成小块内存的自由链表。而page cache 中的spanlist则是按下标桶号映射的,也就是说第i号桶中挂的span都是i页内存
  • PageCache 申请逻辑代码框架:

  1. PageCache.h
class PageCache
{
public:
	static PageCache* getInstance()
	{
		return &_Inst;
	}
	Span* newSpan(size_t k); // 获取一个K页的span
	
	std::mutex _pageMtx;
private:
	PageCache(){}
	PageCache(const PageCache&) = delete;

	SpanList _spanLists[NPAGES];
	static PageCache _Inst;
};
  1. PageCache.cpp
PageCache PageCache::_Inst;

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

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

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

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

		_spanLists[bigSpan->_n].pushFront(bigSpan);
		return newSpan(k);
	}
}
  1. 此时Common.h里的方法增加如下
static const size_t NPAGES = 129;

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

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

其中,疑难点在于:在这里插入图片描述

3.3.4 申请逻辑结构图

在这里插入图片描述

3.4 释放内存的逻辑

3.4.1 thread cache的释放逻辑

  • 释放内存:
    1. 当释放内存小于256k时将内存释放回thread cache,计算size映射自由链表桶位置i,将对象Push到_freeLists[i]
    2. 当链表的长度过长,则回收一部分内存对象到central cache
  • ThreadCache 释放逻辑代码框架:
  1. ThreadCache.h
class ThreadCache
{
public:
	void* allocate(size_t bytes); // 申请内存对象
	void deallocate(void* ptr, size_t bytes); // 释放内存对象
	void* fetchFromCentralCache(size_t index, size_t bytes); // 从中心缓存获取对象
	void ListTooLong(FreeList& list, size_t bytes); // 释放对象时,链表过长时,回收内存到中心缓存
private:
	FreeList _freeLists[NFREELIST];
};

// TLS thread local storage 线程局部存储的声明
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;
  1. ThreadCache.cpp
void ThreadCache::deallocate(void* ptr, size_t bytes)
{
	assert(ptr);
	assert(bytes <= MAX_BYTES);

	// 找对映射的自由链表桶,对象插入进入
	size_t index = SizeClass::index(bytes);
	_freeLists[index].push(ptr);

	// 当链表长度大于一次批量申请的内存时就开始还一段list给central cache
	if (_freeLists[index].size() >= _freeLists[index].maxSize())
	{
		ListTooLong(_freeLists[index], bytes);
	}
}
void ThreadCache::ListTooLong(FreeList& list, size_t bytes)
{
	void* start = nullptr;
	void* end = nullptr;
	list.popRange(start, end, list.maxSize());
	// 把切出的list还给中心缓存,此时只需要给start,因为该list的end直接遍历到空就是了
	CentralCache::getInstance()->releaseListToSpans(start, bytes);
}
  1. 此时Common.h里的方法增加如下
// 管理切分好的自由链表
class FreeList
{
public:
	void push(void* obj)
	{
		assert(obj); // ************
		//头插
		NextObj(obj) = _freeList;
		_freeList = obj;
		++_size;
	}
	void pushRange(void* start, void* end, size_t len)
	{
		NextObj(end) = _freeList;
		_freeList = start;
		_size += len;
	}
	void* pop()
	{
		assert(_freeList); // **************
		//头删
		void* obj = _freeList;
		_freeList = NextObj(obj);
		--_size;

		return obj;
	}
	void popRange(void*& start, void*& end, size_t len)
	{
		assert(len <= _size);

		start = _freeList;
		end = start;
		for(size_t i = 0; i < len - 1; ++i)
		{
			end = NextObj(end);
		}
		_freeList = NextObj(end);
		NextObj(end) = nullptr;
		_size -= len;
	}
	bool empty()
	{
		return _freeList == nullptr;
	}
	size_t& maxSize()
	{
		return _maxSize;
	}
	size_t size()
	{
		return _size;
	}
private:
	void* _freeList = nullptr;
	size_t _maxSize = 1;
	size_t _size = 0;
};

3.4.2 central cache的释放逻辑

  • 释放内存:
    1. 当thread_cache过长或者线程销毁,则会将内存释放回central cache中的,释放回来时--use_count。当use_count减到0时则表示所有对象都回到了span,则将span释放回page cache,page cache中会对前后相邻的空闲页进行合并
  • CentralCache 释放逻辑代码框架:
  1. CentralCache.h

//单例模式————饿汉模式
class CentralCache
{
public:
	static CentralCache* getInstance()
	{
		return &_Inst;
	}
	Span* GetOneSpan(SpanList& list, size_t bytes); // 获取一个非空的span
	size_t fetchRangeObj(void*& start, void*& end, size_t batchNum, size_t bytes); // 从中心缓存获取一定数量的对象给thread cache
	void releaseListToSpans(void* start, size_t bytes); // 将一定数量的对象释放到span跨度
private:
	CentralCache(){}
	CentralCache(const CentralCache&) = delete;

	SpanList _spanLists[NFREELIST];
	static CentralCache _Inst;
};
  1. CentralCache.cpp
void CentralCache::releaseListToSpans(void* start, size_t bytes)
{
	size_t index = SizeClass::index(bytes);
	_spanLists[index]._mtx.lock();

	while (start)
	{
		void* next = NextObj(start);
		Span* span = PageCache::getInstance()->MapObjectToSpan(start);
		NextObj(start) = span->_freeList;
		span->_freeList = start;
		span->_useCount--;

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

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

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

		start = next;
	}

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

其中,疑难点在于:在这里插入图片描述

  1. 此时Common.h里的方法增加如下
// 管理多个连续页大块内存跨度结构
struct Span
{
	PAGE_ID _pageId = 0; // 页号
	size_t _n = 0;       // 页数

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

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

	bool _isUse = false;
};

其中,疑难点在于:在这里插入图片描述

3.4.3 page cache的释放逻辑

  • 释放内存:
    1. 如果central cache释放回一个span,则依次寻找span的前后page id的没有在使用的空闲span,看是否可以合并,如果合并继续向前寻找。这样就可以将切小的内存合并收缩成大的span,减少内存碎片
  • PageCache 释放逻辑代码框架:
  1. PageCache.h
class PageCache
{
public:
	static PageCache* getInstance()
	{
		return &_Inst;
	}
	Span* newSpan(size_t k); // 获取一个K页的span
	Span* MapObjectToSpan(void* obj); // 获取从对象到span的映射
	void ReleaseSpanToPageCache(Span* span); // 释放空闲span回到Pagecache,并合并相邻的span

	std::mutex _pageMtx;
private:
	PageCache(){}
	PageCache(const PageCache&) = delete;

	SpanList _spanLists[NPAGES];
	static PageCache _Inst;
	std::unordered_map<PAGE_ID, Span*> _idSpanMap;
};

其中,疑难点在于:在这里插入图片描述

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

	std::unique_lock<std::mutex> lock(_pageMtx); // 加锁(RAII)

	auto ret = _idSpanMap.find(id);
	if (ret != _idSpanMap.end())
	{
		return ret->second;
	}
	else
	{
		assert(false);
		return nullptr;
	}
}
void PageCache::ReleaseSpanToPageCache(Span* span)
{
	// 大于128 page的直接还给堆
	if (span->_n > NPAGES - 1)
	{
		void* ptr = (void*)(span->_pageId << PAGE_SHIFT);
		SystemFree(ptr);

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

		Span* preSpan = ret->second;
		if (preSpan->_isUse == true) break; // 前面相邻页的span在使用,不合并了
		if (preSpan->_n + span->_n > NPAGES - 1) break; // 合并出超过128页的span没办法管理,不合并了

		//可以合并了
		span->_pageId = preSpan->_pageId;
		span->_n += preSpan->_n;
		//被合并了就应该清理
		_spanLists[preSpan->_n].erase(preSpan);

		delete preSpan;
	}	
	// 向后合并
	while (true)
	{
		PAGE_ID nextId = span->_pageId + span->_n;
		auto ret = _idSpanMap.find(nextId);
		if (ret == _idSpanMap.end()) break; // 前面的span不存在,不合并了

		Span* nextSpan = ret->second;
		if (nextSpan->_isUse == true) break;
		if (nextSpan->_n + span->_n > NPAGES - 1) break;

		span->_n += nextSpan->_n;

		_spanLists[nextSpan->_n].erase(nextSpan);
		delete nextSpan;
	}
	_spanLists[span->_n].pushFront(span);
	span->_isUse = false;
	_idSpanMap[span->_pageId] = span;
	_idSpanMap[span->_pageId + span->_n - 1] = span;
}

3.4.4 申请逻辑结构图

在这里插入图片描述

3.5 大于256k的内存申请逻辑

  • 此时需要完善一些代码
  1. ConcurrentAlloc.h
static void* ConcurrentAlloc(size_t bytes)
{
	// 申请大块内存(256k < bytes),直接跳过thread cache和central cache,去page cache里申请newSpan
	if (bytes > MAX_BYTES)
	{
		size_t align = SizeClass::roundUp(bytes);
		size_t kpage = align >> PAGE_SHIFT;

		PageCache::getInstance()->_pageMtx.lock();
		Span* span = PageCache::getInstance()->newSpan(kpage);
		span->_objSize = bytes;
		PageCache::getInstance()->_pageMtx.unlock();

		void* ptr = (void*)(span->_pageId << PAGE_SHIFT);
		return ptr;
	}
	else
	{
		// 通过TLS 每个线程无锁地获取自己专属的ThreadCache对象
		if (pTLSThreadCache == nullptr)
		{
			pTLSThreadCache = new ThreadCache;
		}
		return pTLSThreadCache->allocate(bytes);
	}
}

static void ConcurrentFree(void* ptr)
{
	Span* span = PageCache::getInstance()->MapObjectToSpan(ptr);
	size_t bytes = span->_objSize;
	if (bytes > MAX_BYTES)
	{
		PageCache::getInstance()->_pageMtx.lock();
		PageCache::getInstance()->ReleaseSpanToPageCache(span);
		PageCache::getInstance()->_pageMtx.unlock();
	}
	else
	{
		assert(pTLSThreadCache);
		pTLSThreadCache->deallocate(ptr, bytes);
	}
}
  1. PageCache.cpp => Span* PageCache::newSpan(size_t k)
// 获取一个K页的span
Span* PageCache::newSpan(size_t k)
{
	assert(k > 0);

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

		_idSpanMap[span->_pageId] = span;
		return span;
	}
	else
	{
		// 同上...
	}
}
  1. PageCache.cpp => void PageCache::ReleaseSpanToPageCache(Span* span)
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;
	}
	// 同上...
}
  1. 此时Common.h里的方法增加如下
// 直接去堆上按页申请空间
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 inline size_t roundUp(size_t bytes)
{
	if (bytes <= 128)
	{
		return __roundUp(bytes, 8);
	}
	else if (bytes <= 1024)
	{
		return __roundUp(bytes, 16);
	}
	else if (bytes <= 8 * 1024)
	{
		return __roundUp(bytes, 128);
	}
	else if (bytes <= 64 * 1024)
	{
		return __roundUp(bytes, 1024);
	}
	else if (bytes <= 256 * 1024)
	{
		return __roundUp(bytes, 8 * 1024);
	}
	else
	{
		return __roundUp(bytes, 1 << PAGE_SHIFT); // 修改这里
	}
}

3.6 用定长内存池替代new(malloc)

  • 我们在3.1写的定长内存池是直接从堆申请内存的,所以采用它之后,这个项目也就彻底与malloc解耦了
  • 并且该定长内存池效率会比malloc高
  • 此时对以下代码进行调整
  1. PageCache.h
    在这里插入图片描述
  2. PageCache.cpp
    在这里插入图片描述
  3. ConcurrentAlloc.h
    在这里插入图片描述

四、多线程场景测试

4.1 构造多线程并发场景

  • 多线程并发环境下,对比malloc和ConcurrentAlloc申请和释放内存效率对比
  • 导入BenchMark.cpp进行测试
// 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 + 0);
	printf("%u个线程并发执行%u轮次,每轮次free %u次: 花费:%u ms\n", nworks, rounds, ntimes, free_costtime + 0);
	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]);
				}
				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 + 0);
	printf("%u个线程并发执行%u轮次,每轮次concurrent dealloc %u次: 花费:%u ms\n", nworks, rounds, ntimes, free_costtime + 0);
	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;
}

测试结果为:在这里插入图片描述

4.2 使用性能测试工具分析瓶颈

  • 可以看到,我们的高并发内存池比起malloc还是差了不少
  • 为了确定我们的内存池的瓶颈,下面采用VS2019自带的性能检测工具
  1. 调试选项中选择性能探查器
    在这里插入图片描述
  2. 开始检测,等待分析在这里插入图片描述
  3. 分析性能瓶颈,可以看到ConcurrentFree()占用时间最多,究其原因是因为加锁
    在这里插入图片描述

4.3 突破瓶颈的方案

  • 翻阅tcmalloc源码,发现其使用基数树进行优化
  • 单层基数树代码
// Single-level array
template <int BITS>
class TCMalloc_PageMap 
{
private:
	static const int LENGTH = 1 << BITS;
	void** array_;

public:
	typedef uintptr_t Number;

	explicit TCMalloc_PageMap() 
	{
		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);
	}
	void* get(Number k) const 
	{
		if ((k >> BITS) > 0) 
		{
			return nullptr;
		}
		return array_[k];
	}
	void set(Number k, void* v) 
	{
		array_[k] = v;
	}
};
  • 于是修改以下代码:
  1. PageCache.h
    在这里插入图片描述
  2. PageCache.cpp在这里插入图片描述
  • 此时再运行一次代码,发现性能极大地提高在这里插入图片描述
  • 再用性能检测工具检测看看,可以发现瓶颈不再是加锁问题,因为基数树代替了unordered_map,不需要加锁在这里插入图片描述
  • 分析:
    1. 基数树底层其实就是采用直接定址法的哈希,对于咱这个内存池,可以提前开好空间
    2. unordered_map插入时可能因为扩容而调整结构
    3. 基于以上两点,unordered_map的读写不分离,需要加锁。避免一个线程在读的时候另一个线程在写并改变了结构
    4. 而基数树提前开了空间,插入数据不可能扩容,不会改变结构。因此读写分离,读不需要加锁,因为要读的span和要写的span不会冲突(直接定址法)

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

CGod

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

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

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

打赏作者

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

抵扣说明:

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

余额充值