【模拟tcmalloc】(一)定长内存实现

2023-12-13 22:05:32

一.需求和特点

固定大小的内存申请释放需求,特点是性能达到极致,不考虑内存碎片问题

二.整体设计

1.主体内存

每次申请一大块内存作为内存池,用一次切一块。需要用指针管理没切分的内存块,用char类型的指针是最好的选择,指针移动的步长是自己类型的大小,char本身是1,能凑出任意步长。

2.释放链表

我们还需要一个指针管理释放回来的空间,因为释放回来的内存块是乱序的所以void类型的指针即可。同时归还的内存块需要整体管理起来,我们就需要一个next指针。我们不用再多定义一个指针,因为归还回来的内存块就可以被使用,我们直接让内存块的位置放上next指针即可,这意味着内存块的大小必须大于4或者8,不然存不下next指针

3.内存统计

不能通过主体内存的控制指针判空来确定是否有空间。因为就算所有的内存都被使用了地址也是不是空的。我们需要手动引入一个标记剩余空间大小的变量

4.对象释放环节

指针之间可以强制类型转换我们想在内存开头4字节写地址的话可以强转成int*,这时在解引用就是开头的4字节。不过仍然有问题,64位下一个指针8字节,这时就放不下。我们可以强转成任意二级指针类型,因为指针的大小是随着系统会发生改变的,4或者8字节,这时在解引用就会拿到的就是当前系统下的一个指针大小就能自动识别了。??

5申请流程

优先是由释放链表的内存块,不够了在去大内存切

6.用系统内存申请函数替换malloc

linux用mmap或者brk。

void * sbrk(intptr_t increment);

brk是将数据段(.data)的最高地址指针_edata往高地址推;mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。

?????这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

windows 用VirtualAlloc

LPVOID VirtualAlloc(LPVOID lpAddress,SIZE_T dwSize,DWORD  flAllocationType,DWORD  flProtect);

用于在进程的虚拟地址空间中分配内存。申请内存常用的参数

  • lpAddress:指定要分配内存的首选地址。可以传递?NULL,表示让系统自动选择一个合适的地址。
  • dwSize:要分配的内存大小,以字节为单位。
  • flAllocationType:指定内存分配的类型。常用的取值包括:
    • MEM_COMMIT:将分配的内存提交为物理内存。
    • MEM_RESERVE:保留一段地址空间以供后续使用,但不提交物理内存。
  • flProtect:指定内存保护属性。常用的取值包括:
    • PAGE_EXECUTE_READWRITE:可执行、可读、可写的内存。
    • PAGE_READWRITE:可读、可写的内存。

三.代码

//定长的内存池,只能用于申请同类的对象
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 = capacity;				//右移计算多少页
				_memory = (char*)(Memreq::SystemAlloc(capacity>>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 (obj)T;  //定位new,构造T
		return obj;
	}

	void Delete(T* obj)
	{
		obj->~T();  //定位new必须显示调用析构
		//头删的思路
		//因为不知道是32还是64位机器,直接转换成指针就能自动识别了
		*(void**)obj = _freelist;//当前位置链接后序节点
		//不能直接写void*,上面是指针解引用是左值,(void*)obj 是一个临时的、不具有内存地址的值,它不能作为赋值的目标,是一个右值。
		_freelist = obj;//当前位置成为新的头

	}

private:
	char* _memory = nullptr; //指向整个未被分配的内存主体,char类型更方便管理
	size_t _remainBytes = 0;  //剩余未使用字节数,不能通过_memory判空来确定是否有空间。因为就算所有的内存都被使用了地址也是不是空的
	void* _freelist = nullptr;	//使用完毕还回来的内存块,以链表的形式链接。不用next指针
								//用内存块的前4个或者8个字节存放下一个内存块的地址
};

内存申请模块,linux下没有测试过,win下通过测试?

?

	inline static void* SystemAlloc(size_t kpage)
	{
#ifdef _WIN32
		//默认一页8k大小	2的几次方(3(8),10(1024)一共8k)		1.分配起始地址2.分配内存大小为几页3.类型4.保护属性
		void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);		//1.为指定地址空间提交物理内存。2.保留指定地址空间阻止其他内存分配函数malloc和LocalAlloc等再使用已保留的内存范围,直到它被被释放,3.应用程序可以读写该区域。
		if (ptr == nullptr)
		{
			throw std::bad_alloc();
		}
#elif __linux__	//brk或者mmap
		void* ptr = sbrk(0); // 获取当前 break 地址
		void* newBrk = currentBrk + kpage << 13; // 计算新的 break 地址
		if (brk(newBrk) != 0)
		{
			throw std::bad_alloc();
		}
#endif
		return ptr;
	}
	

文章来源:https://blog.csdn.net/m0_62572672/article/details/134971946
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。