C++项目:高并发内存池

ConcurrentMemoryPool原码链接:https://gitee.com/yangyangqingxuan/c-learning-code/tree/b5a78cbe774189769d9a4951c9e48d93f80d026e/ConcurrentMemoryPool2

1.需求分析

C/C++下内存管理是件很头疼的事,分配足够的内存、追踪内存的分配、在不需要的时候释放内存—这个任务很复杂。如果直接使用系统调用malloc/free、new/delete进行内存分配和释放,则会有以下弊端:
1.调用malloc/new,系统需要根据“最先匹配”、“最优匹配”或其他算法在内存空闲块表中查找一块空闲内存,调用free/delete,系统可能需要合并空闲内存块,这些会产生额外开销;
2.频繁使用时会产生大量内存碎片,从而降低程序运行效率;
3.容易造成内存泄漏;
内存池(memory pool)是代替直接调用malloc/free、new/delete进行内存管理的常用方法,当我们申请内存空间时,首先到我们的内存池中查找合适的内存块,而不是直接向操作系统申请,优势在于:
1.比malloc/free进行内存申请/释放的方式快;
2.不会产生或很少产生堆碎片;
3.可避免内存泄漏;

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

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

1.性能问题
2.多线程环境下,内存申请和内存释放所引起的锁竞争问题
3.内存碎片问题

Concurrent Memory pool主要由以下3个部分构成:

1.Thread Cache:
线程缓存是每个线程独有的,用于小于256KB的内存的分配,线程从这里申请内存不需要加锁,每个线程独享一个Cache,这也就是这个并发线程池高效的地方。(这里使用了TLS(thread local storage)线程本地存储,是每个线程独有的全局变量)

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对象,并且合并相邻的页,组成更大的页,缓解内存碎片(外碎片)的问题。

在这里插入图片描述
怎么实现每个线程都拥有自己唯一的线程缓存呢?
静态详解链接:https://blog.csdn.net/evilswords/article/details/8191230
动态详解链接:https://blog.csdn.net/yusiguyuan/article/details/22938671

3.内部细节构成介绍

3.1高并发内存池-Thread Cache

Thread Cache.h

#pragma once

#include "Common.h"

class ThreadCache
{
public:
	// 申请和释放内存对象
	void* Allocate(size_t size);
	void Deallocate(void* ptr, size_t size);

	// 从中心缓存获取对象
	void* FetchFromCentralCache(size_t index, size_t size);

	// 释放对象时,链表过长时,回收内存回到中心缓存
	void ListTooLong(FreeList& list, size_t size);
private:
	FreeList _freeLists[NFREELIST];
};

// TLS thread local storage
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;

Thread Cache是哈希桶结构,每个桶是一个按桶位置映射大小的内存块对象的自由链表。每个线程都会有一个Thread Cache对象,这样每个线程在这里获取对象和释放对象时是无锁的,大大提高了内存池的效率。
不分段的话若全是按8字节分,则会需要32768个桶,但但是每个桶都是16Byte则会差生较多的内存碎片。因此这里采用了不同段的内存使用不同的内存对齐规则,既控制了桶的数量不会太多,又整体将内存碎片浪费控制在10%左右
在这里插入图片描述
管理对齐和映射的关系如下

[1,128]                     8byte对齐      _freelist[0,16)
[129,1024]                  16byte对齐     _freelist[16,72)
[1025,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)
内碎片的计算方法:
对于7/8;15/(128+16);127/(1024+128);1023/(8*1024+1024);
对于进行哈希映射的数组的大小是208
208 = (128/8) + (1024-128)/16 + (8*1024-1024)/128 + (64*1024-8*1024)/1024 + (256*1024-64*1024)/(8*1024)

内存碎片浪费率分别为:7/8,15/144,127/1152,1023/8 * 1024 + 1024均在10%左右(除了第一块按照8字节对齐的浪费率)。
对齐映射规则

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//size > 256 * 1024 byte 
		{
			return _RoundUp(size, 1<<PAGE_SHIFT);
		}
	}

	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[0] + group_array[1];
		}
		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)
		{
			return _Index(bytes - 64 * 1024, 13) + group_array[0] + group_array[1] + group_array[2] + group_array[3];
		}
		else
		{
			assert(false);
			return -1;
		}
	}

在这里插入图片描述
Thread Cache,h

#pragma once
#include"Common.h"
class ThreadCache
{
public:
	// 申请和释放内存对象
	void* Allocate(size_t size);

	void Deallocate(void* ptr, size_t size);//给定size,计算出在哪个桶

	//从中心获取对象
	void* FetchFromCentralCache(size_t index, size_t size);

	//释放对象时,链表过长,回收内存回到中心缓存
	void ListTooLong(FreeList& list, size_t size);
private:
	FreeList _freeLists[NFREELISTS];
};
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;

申请内存:
①:当申请内存size <=256k时,在Thread Cache中申请内存,通过size计算出自由链表中的桶的位置,如果自由链表对应的桶中有内存,则直接从Free List[i]的首部取出一块内存,因为这里使用的是哈希映射,并且没有锁竞争,因此时间复杂度位O(1)。
②③:当Free List中没有对象时,则会批量的从Central Cache中获取一定数量的内存,返回一个内存并将之前批量申请的剩余的内存挂到对应free list桶中。

释放内存:
当FreeList[i]中没有对象时,则批量从Central cache中获取一定数量的对象,剩余的n-1个对象插入到自由链表并返回一 个对象。

3.2高并发内存池-Central Cache

在这里插入图片描述

① Central Cache本质是由一个哈希映射的span对象自由双向链表构成
②为了保证全局只有唯一的Central Cache,这个类因此可以被设计称单例模式(这里使用的是饿汉模式)
饿汉模式:构造函数私有,对象设为静态私有。拷贝构造和赋值重载设为delete(防拷贝)

Central Cache.h

#pragma once
#include"Common.h"


//单例模式 -》饿汉模式
// 采用饿汉模式,在main函数之前单例对象已经被创建
class CentralCache
{
public:
	static CentralCache* GetInstance()
	{
		return &_sInst;
	}
	// 获取一个非空的span
	Span* GetOneSpan(SpanList& list, size_t size);
	// 从中心缓存获取一定数量的对象给thread cache
	size_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size);

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

private:
	SpanList _spanLists[NFREELISTS];
private:
	CentralCache()
	{}
	CentralCache(const CentralCache&) = delete;


	static CentralCache _sInst;
}; 

span对象:一个由多个页组成的内存块,每页大小是8K。
Span对象

//管理多个连续页的大块内存跨度结构
struct Span
{
	PAGE_ID _pageId = 0;//大块内存起始页的页号
	size_t _n = 0;//页的数量

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

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

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

SpanList:一个双向链表,插入删除效率较高

//带头双向循环链表
class SpanList
{
public:
	SpanList()
	{
		_head = new Span;
		_head->_next = _head;
		_head->_prev = _head;
	}

	void Insert(Span* pos, Span* newSpan)
	{
		assert(pos);
		assert(newSpan);
		Span* prev = pos->_prev;
		prev->_next = newSpan;
		newSpan->_prev = prev;
		newSpan->_next = pos;
		pos->_prev = newSpan;
	}

	Span* Begin()
	{
		return _head->_next;
	}

	Span* End()
	{
		return _head;
	}

	bool Empty()
	{
		return _head->_next == _head;
	}
	void PushFront(Span* span)
	{
		Insert(Begin(), span);
	}

	Span* PopFront()
	{
		Span* front = _head->_next;
		Erase(front);
		return front;
	}
	void Erase(Span* pos)
	{
		assert(pos);
		assert(pos != _head);

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

Central Cache申请内存
1.当Thread Cache中没有内存时,就会批量向Central Cache申请一些内存对象,这里的批量获取对象的数量采用了类似网络tcp协议中拥塞控制的满开始算法;Central Cache也有一个哈希映射spanlist,spanlist中挂着span对象,从span中取出对象给ThreadCache,这个过程是需要加锁的,这里使用的是一个桶锁,尽可能提高效率(桶锁限制的只是当两个线程取相同size的内存块时才会发生线程锁竞争)。

2.Central Cache映射的spanlist中所有span对象都没有内存以后,则需要向PageCache申请一个新的span对象,拿到span对象之后,将span管理的内存块按大小切好作为自由链表连接到一起,然后从span中取对象返回给ThreadCache.

3.Central Cache的中挂的span中use_count记录了该span借给ThreadCache多少个对象出去,每借出一个use_count++。当这个span的使用计数为0,说明这个span所有的内存对象都是空闲的,然后将它交给Page Cache合并成更大的页,减少内存碎片(外碎片)。起到承上启下的作用。

Central Cache释放内存
当thread cache过长或者线程销毁,则会将内存释放回Central cache中,每释放一个内存对象,检查该内存所在的span使用计数是否为空,释放回来一个时–use_count。

当use_count减到0时则表示所有对象都回到了span,则将span释放回Page cache,在Page cache中会对前后相邻的空闲页进行合并。

如何将Thread Cache中的内存对象回收到Central Cache中呢?
实际上每当Page Cache借出一定数量页的span时,就会对该span中的页号和该span一 一建立映射,并且将剩余的span对象重新挂到其对应的的桶上,并将起始页和尾页与剩余的span建立映射,以确保往后回收的时候,可以进行前后页合并。达到了内存分配在多个线程中更均衡的按需调度的目的。

3.3高并发内存池-Page Cache

在这里插入图片描述

页缓存是在Central Cache缓存上面的一层缓存,存储的内存是以页为单位存储及分配的,Central Cache没有内存对象时,从Page Cache分配出一定数量的Page,并切割定长大小的小块内存,分配给Central
Page Cache是一个以页为单位的span自由链表。
为了保证全局只有唯一的Page Cache,这个类被设计成了单例模式。采用了饿汉模式。


#pragma once
#include"Common.h"
#include"ObjectPool.h"
#include"PageMap.h"
//采用饿汉模式,在main函数之前单例对象已经被创建
class PageCache
{
public:
	static PageCache* GetInstance()
	{
		return &_sInst;
	}

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

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


	 在SpanList中获取一个K页的Span对象,如果没有或者申请内存大于128页,则直接去系统申请
	Span* NewSpan(size_t k);
	// 为了锁住SpanList,可能会存在多个线程同时来PageCache申请span
	std::mutex _pageMtx;
private:
	PageCache() {};
	ObjectPool<Span> _spanPool;
	SpanList _spanList[NPAGES];
	//std::unordered_map<PAGE_ID, Span*> _idSpanMap;
	TCMalloc_PageMap1<32 - PAGE_SHIFT> _idSpanMap;
	PageCache(const PageCache&) = delete;
	PageCache& operator=(const PageCache&) = delete;

	static PageCache _sInst;
};

PageCache没有采用桶锁而使用了整体的大锁的原因:
频繁调度锁资源,会引起线程的频繁切换,这会使得上下文数据被频繁的保存,严重占用CPU资源。

Page Cache申请流程:
1.当CentralCache向PageCache申请内存时,page Cache先检查的是对应位置有没有span,若没有则向更大的页寻找一个span,例如申请的是4页,4页page后面没有挂span,则向后面寻找更大的span,假设在10页page位置找到一个span,则将10页page的span分裂成一个4页的page span 和一个6页page span。
2.若找到_spanList[128]都没有合适的span,则向系统使用VirtualAlloc或者mmap,brk等方式申请128页page span关在自由链表中,在重复第一点中的步骤,对其进行切分。

brk,mmap,malloc底层详解:https://blog.csdn.net/gfgdsg/article/details/42709943

3.Central Cache 和 Page Cache的核心结构都是spanlist的哈希桶,但是他们是有本质区别的,Central Cache中的哈希桶,是按跟Thread Cache一样大小对齐关系映射的,他的spanlist中挂的span中的内存都被按映射关系切好链接成小块内存的自由链表。而page cache中的spanlist则是按下标桶号映射的,也就是说第i号桶中挂的span都是i页内存。

4.申请内存流程

Linux平台
使用brk或mmap向系统直接申请堆内存

申请小于128k的内存,使用brk分配内存,将_edata往高地址推(只分配虚拟空间,不对应物理内存(因此没有初始化),第一次读/写数据时,引起内核缺页中断,内核才分配对应的物理内存,然后虚拟地址空间建立映射关系)。

申请大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配(对应独立内存,而且初始化为0),
Windows平台下使用VirtualAlloc向系统申请和释放堆内存

//直接去堆上按页申请空间
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
	void* ptr = VirtualAlloc(0, kpage * (1 << 13), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);//kpage是字节大小
#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 // _WIN32
}

4.1向系统申请内存

我们的高并发内存池项目对外仅提供两个接口,对于申请内存的接口是ConcurrentAlloc()
①如果申请的内存超过256 K(32页),则直接会越过Thread Cache去调用Page Cache的NewSpan()函数,NewSpan的功能是获取一个K页的span对象,这里有两种情况,第一种情况是申请的页数介于32页和128页之间,若PageCache里面有,则向PageCache索要,若索要的页数所在的spanList链表为空则向后寻找更大的页,若一直没有相匹配的页,则最终会向堆申请128页,然后进行切分,返回你申请的那一部分内存。第二种情况是申请的页数大于128页,这种情况会直接调用VirtualAlloc()向系统申请内存。

②如果申请的内存小于256 K,则会先在线程独享的ThreadCache里所提供的Allocate()接口,计算出要找哪一个索引下标index的freelist,如果有则直接返回。没有就会调FetchFromCentralCache接口,通过你要的内存size计算出需要返回的批量个数(慢启动方式),并且调用CentralCache的FetchRangeObj()计算出Central Cache中心缓存实际真正能给你返回的数量,这时就得调用Central Cache 中的GetOneSpan()接口在获取一个非空的SpanCentral Cache ,如果Central Cache中对应的SpanList[i]中没有空闲的Span或者内存被用完了,这时就找Page Cache要,通过调用Page Cache中的New Span接口来获取一个K页的span对象,计算索引看Page Cache中是否有合适的页,如果没有则需要往后找更大的页,如果一直没找到,就只能向系统申请一个128页的内存了,然后进行切分,由于Central Cache中的span都是已经切好的,因此在返回这K页的内存时,需要提前按照之前计算字节向上对齐的大小切好,然后再返回给Central Cache,然后计算出Central Cache中心缓存实际真正能给你返回的数量。然后返回的内存块挂接到Thread Cache的freeList[i]上,取出一个内存块返回给用户。

5.释放内存流程

还回来的内存挂接在Thread Cache中对应的Free List中,当其中一个Free List中的Size(当前的内存块数)大于Max Size时,从该Free List取出Max Size个内存归还到Central Cache,但是每一块小内存都可能来自于不同的Span,这里就要调用MapObjectToSpan()接口根据每块小内存的起始地址计算出它所对应的页号,通过一个基数树映射根据页号找到所对应的Span,那么就可以确保每一小块内存都能归还到之前所切出来的对应的Span中,当Central Cache中的每一大块Span里面有一个usecount,他记录的是分配出去的内存块数,当他为0时,说明该span对象之前借出去的小内存都换回来了,这时就可以将该span归还给Page Cache,通过PageId(页号)和n(页数),进行在Page Cache中向前和向后合并,形成更大的页,当来缓解内存碎片问题。

6.性能测试

测试平台Visual Studio 2019

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.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> 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.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, malloc_costtime.load() + free_costtime.load());
}

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

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

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

可看出消耗cpu占用率,malloc,free这两库函数占比率最大,是系统开销大的主要原因。

7.项目不足及扩展学习

项目的独立性不足

1.不足
①当前实现的项目中我们并没有完全脱离malloc,比如在内存池自身数据结构的管理中,如SpanList中的span等结构,我们还是使用的new Span这样的操作,new的底层使用的是malloc,所以没有完全脱离malloc。

②按理来说连thread、mutex等库里面的也不可以使用,因为你不确定是否他们的库里实现使用了new或者malloc申请内存
③如果此时threadCache销毁了,如果他还有大量内存未释放这样会造成严重的内存泄漏,这时该怎么办?

2.解决方案
①项目中增加一个定长的ObjectPool的对象池,对象池的内存直接使用brk(Linux平台)、VirarulAlloc(Windows平台)等向系统申请,使用定长内存池,在new span时向内存池中获取内存,定长内存池的内存是通过SystemAlloc()来获取内存的会脱离malloc的调用

定长内存池代码实现如下

#pragma once
#include"Common.h"
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)
	{
		//显示调用析构函数清理对象
		obj->~T();

		*(void**)obj = _freeList;
		_freeList = obj;
	}


private:
	char* _memory = nullptr;//指向大块内存的指针 (char*   方便于从内存中取内存,+-操作方便)
	size_t _remainBytes = 0;//大块内存在被切分过程中剩余的字节数
	void* _freeList = nullptr;//自由链表:回收不再使用的内存

};

②不同平台替换方式不同。 基于lunix的系统上的glibc,使用了weak alias的方式替换。具体来说是因为这些入口函数都被定义成了weak symbols。
再加上gcc支持 alias attribute,所以替换就变成了这种通用形式:
void* malloc(size_t size) THROW attribute__ ((alias (tc_malloc))),因此所有malloc的调用都跳转到了tc_malloc的实现

GCC attribute 之weak,alias属性(实际上就是当我们调用malloc的时候,他会自动跳转到我们自己实现的tc_malloc)
https://blog.csdn.net/BingoAmI/article/details/78683906

有些平台不支持这样的东西,需要使用hook的钩子技术来做。
https://www.cnblogs.com/feng9exe/p/6015910.html

③针对上面的第三点会造成耽误内存还回CentralCache,导致小页无法合大,注册一个线程结束后回调清理ThreadCache的内存函数。但是实际上线程局部存储机制中在对象内存在线程开始后分配,线程结束时回收,且每个线程有该对象自己的实例。

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
老师可能会提出以下问题: 1. 什么是内存池?为什么需要内存池? 答:内存池是一种内存管理技术,用于提高内存分配和释放的性能。它通过预先分配一定数量的内存块,并在程序运行期间重复利用这些内存块来避免频繁的内存分配和释放操作,从而提高程序的运行效率。 2. 内存池如何实现高并发? 答:内存池可以通过多线程技术来实现高并发。一般情况下,内存池会将内存块分配给不同的线程进行使用,每个线程都有自己的内存池。当多个线程同时请求内存块时,内存池可以进行加锁操作来保证线程安全。 3. 如何处理内存池中的内存碎片问题? 答:内存池中的内存碎片问题可以通过两种方式来解决。一种是使用内存池的分配算法来减少内存碎片的产生,另一种是定期对内存池进行整理和重组,以消除已有的内存碎片。 4. 如何进行内存池的扩展和收缩? 答:内存池的扩展和收缩可以通过动态调整内存池的大小来实现。当内存池中的内存块被耗尽时,可以重新分配一定数量的内存块,并将它们添加到内存池中。当内存池中的内存块处于空闲状态时,可以将它们从内存池中移除,以释放内存空间。 5. 如何测试内存池的性能? 答:测试内存池的性能可以使用一些基准测试工具,如Google Benchmark等。在测试时,可以比较内存池的分配和释放操作与系统默认的分配和释放操作之间的性能差异。同时,还可以测试内存池高并发环境下的性能表现。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

倚心

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

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

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

打赏作者

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

抵扣说明:

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

余额充值