C++实现高并发内存池

本文详细介绍了如何设计一个高并发内存池,以tcmalloc为原型,涵盖定长内存池、线程缓存、中心缓存和页缓存的实现,以及如何通过基数树优化内存分配。学习了内存池管理、多线程同步和内存碎片处理的关键技术。

完整代码

代码链接

项目介绍

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

tcmalloc源代码

内存池

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

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

内存池主要解决的问题
内存池主要解决的当然是效率的问题,其次如果作为系统的内存分配器的角度,还需要解决一下内存碎片的问题。
在这里插入图片描述

上图是外碎片问题。
1、外部碎片是一些空闲的连续内存区域太小,这些内存空间不连续,以至于合计的内存足够,但是不能满足一些的内存分配申请需求。
2、内碎片是由于一些对齐的需求,导致分配出去的空间中一些内存无法被利用。

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

开胃菜–先设计一个定长的内存池

先熟悉一下简单内存池是如何控制的,这个定长内存池也会作为后面内存池的一个基础组件

解决固定大小的内存申请释放需求
特点:
1、性能达到机制
2、不考虑内存脆片等问题

在这里插入图片描述

首先申请一块大块空间,用_memory指向这块空间。

比如从_memory中分别取出内存块A、内存块B、内存块C使用(形象上说取出,实际地址空间都还是连续的)。当某个内存块使用完后归还时(假设是B,第一次归还),我们用 void* _freeList 指针存B的地址(头结点),后面要归还的内存块再一一往后链接。
每个内存块的前4/8个字节存储要链接在其后面的内存块的地址。

static void*& NextObj(void* obj) //取obj的头4/8个字节,& 也可以写
{
   
   
	return *((void**)obj);
}

New:从大块空间出取出一块给对象T使用。
不断取出内存块使用,当空间中剩余内存不够一个对象大小时,则丢弃剩余空间,重新开辟新的大块空间。所以需要int _remains变量记录大块空间中剩余的字节数。
如果自由链表中有内存块(_freeList != nullptr),就优先从自由链表中取出内存块使用。如果没有再从大块空间中取。

	T* New()
	{
   
   
		T * obj = nullptr;
		//如果自由链表有对象,直接取一个
		if(_freeList)
		{
   
   
			obj = (T*)_freeList;
			_freeList = *((void**)_freeList);
		}
		else
		{
   
   
			if (_remains < sizeof(T))
			{
   
   
				_remains = 128 * 1024;
				//_memory = (char*)malloc(_remains);
				_memory = (char*)SystemAlloc(_remains >> 13); //除8k
				if (_memory == nullptr)
				{
   
   
					throw std::bad_alloc();
				}
			}

			obj = (T*)_memory;
			size_t objSize = sizeof(T) > sizeof(void*) ? sizeof(T) : sizeof(void*);
			_memory += objSize;
			_remains -= objSize;
		}

		//对已经有的一块空间初始化,使用定位new显示调用T的构造函数初始化
		new(obj)T;
		return obj;
	}

Delete:当归还一个对象时,将对象(内存块)头插到自由链表中,再次重复利用。

	void Delete(T* obj)
	{
   
   
		//显示调用T的析构函数
		obj->~T();
		//头插
		*((void**)obj) = _freeList;
		_freeList = obj;
	}

注意:如果对象T的大小比指针大小还小,那么从大块空间取内存块时就取指针的大小,方便还回来时连接到一起。

申请内存时,我们可以直接调用系统,找堆按页申请内存,脱离malloc,
这里一页给8K。

//直接去堆上按页申请空间
#ifdef _WIN32
	#include <Windows.h>
#else
	//...
#endif

inline static void* SystemAlloc(size_t kpage)
{
   
   
#ifdef _WIN32
	//kpage*8*1024
	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;
}

该定长内存池在接下来的项目中代替new和delete

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

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

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

concurrent memory pool主要由以下3个部分构成:
在这里插入图片描述

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

thread cache

thread cache是哈希桶结构,每个桶是一个按桶位置映射大小的内存块对象的自由链表。

在这里插入图片描述
申请内存:

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

释放内存:

  1. 当释放内存小于256KB时将内存释放回thread cache,计算size映射自由链表桶位置i,将对象Push到_freeLists[i]。
  2. 当链表的长度过长,则回收一部分内存对象到central cache。

管理切分好的小对象的自由链表

  1. Push
    头插,取obj对象头上的4个字节(以下表述都假设是32位系统下),指向自由链表的第一个节点,再让obj变为第一个。
	void Push(void* obj) //插入一个对象到自由链表
	{
   
   
		assert(obj);
		//头插
		//*(void**)obj = _freeList;
		NextObj(obj) = _freeList;
		_freeList = obj;

		++_size;
	}
  1. Pop
    头删。用obj指向第一个节点,再让_freeList指向obj的下一个节点。
	void* Pop()
	{
   
   
		assert(_freeList);

		//头删
		void* obj = _freeList;
		_freeList = NextObj(obj);
		--_size;

		return obj;
	}

计算对象大小的对齐映射规则
当一个对象被切小了挂到自由链表中时,64位下至少也要8字节来存储地址,所以这里刚开始以8字节对齐。但所有都以8字节对齐的话需要建的自由链表太多了,这里用一种规则来对齐。(向上对齐,但会有内碎片浪费)

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)
	{
   
   
		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
		{
   
   
			return _RoundUp(size, 1 << PAGE_SHIFT);
		}
	}

比如说需要129字节的空间,在[128+1, 1024]范围内是按16字节对齐,15÷144=10%左右,即有10%左右的内碎片浪费。

计算映射的哪一个自由链表桶

	//size_t _Index(size_t bytes, size_t alignNum)
	//{
   
   
	//	if (bytes % alignNum == 0)
	//	{
   
   
	//		return bytes / alignNum - 1;
	//	}
	//	else
	//	{
   
   
	//		return bytes / alignNum;
	//	}
	//}

	// 1 << 3 = 8
	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 
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值