简单实现定长内存池

Posted 两片空白

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了简单实现定长内存池相关的知识,希望对你有一定的参考价值。

目录

前言

一.定长内存池

        1.1 概念

        1.2 实现

        1.2.1 成员变量介绍

        1.2.2 成员函数介绍 

        1.2.3细节

        1.3 代码


前言

        我们知道用户申请内存必须通过操作系统,操作系统再从堆上开辟一段空间给用户。但是如果频繁的向操作系统申请空间,导致操作系统需要频繁的为用户开辟空间。但是操作系统需要管理很多东西,这样会导致操作系统的效率变得很低。

        内存池是操作系统开辟的一大块内存,当用户需要申请空间时,直接去内存池申请空间,不需要通过操作系统。只有当内存池空间不够时,再向操作系统申请一大块空间,作为内存池。

        这样用户需要空间时,就不需要频繁的向操作系统要空间。提高了效率。

一.定长内存池

        1.1 概念

        定长内存池:每次从内存池中分割内存的大小是定长的。

        定长内存池是一大块内存空间,每次需要用户需要申请内存时,直接从内存池中分配即可。当用户空间使用完毕,不能直接释放,因为申请的时候申请的时一大块内存,不能释放小部分。定长内存池使用链表将小块内存连接起来,供下一次使用。

        于是当用户向定长内存池申请空间时,定长内存池会先检测链表中是否有小块内存,有直接申请给用户,没有再从内存块中分出一部分给用户。

  • 优点:简单粗暴,分配和释放的效率高,解决实际中特定场景下的问题有效。

        不需要频繁向操作系统申请空间(这是内存池都拥有的优点),由于是定长的,不需要从链表中查找合适大小内存块,只需要直接分配即可。

  • 缺点:功能单一,只能解决定长的内存需求,另外占着内存没有释放

        由于每次从内存池中分割内存的大小是定长的,如果定长内存池作为STL容器的内存池时,需要针对不同的容器,定义不同的定长内存池。因为不同容器的节点大小不同。vector还无法使用。

        1.2 实现

        1.2.1 成员变量介绍

       成员变量包括一个链表头指针和一个大块内存。

	
struct Memory{
	Memory(){
		leftsize = 1024 * 1;
		memory = (char *)malloc(leftsize);
		std::cout << (void *)memory << std::endl;
		head = memory;
	}

	char *memory = nullptr;//内存池,大块内存
	Memory *next = nullptr;//指向下一个大块内存
	size_t leftsize = 0;//剩余空间
	char *head = nullptr;
	~Memory(){
		free(head);
	}
};
    
//将大的内存块连接起来,方便释放
Memory *m = nullptr;
void *freelist = nullptr;//自由链表头指针,保存用户使用完的空间

 为什么链表头节点直接定义成void*指针,而不是一个结构指针?

        链表的结点不需要保存有效值,只需要保存下一个内存块的地址即可。我们直接利用切割的内存块空间来保存下一个内存块的地址。而void*可以接收任何指针类型。

        比如:在32为平台下,利用分割内存块的前4字节来保存下一个内存块的地址。

 为什么大块内存封装成一个结构体,而不是直接设置成一个指针?

        为了方便释放空间,下面介绍。

结构体变量大块内存为什么char*类型?

        char*步长为1字节,方便计算用户申请的空间。

        1.2.2 成员函数介绍 

        定长内存池提供两个接口给用户,一个是向内存池申请空间,一个是将申请的空间归还给内存池。

向内存块申请空间

  • 先检测自由链表是否有内存块
  • 有内存块,直接头删内存块给用户。
  • 如果没有,向大块内存切割内存块。如果,没有大块内存,需要向系统申请。如果有内存,直接切割内存给用户。
  • 需要记录内存块的剩余空间大小,防止申请越界。如果剩余空间不够,需要向系统申请大块内存。

将申请的空间归还给内存池

  • 直接将归还的空间头插到自由链表中

        1.2.3细节

从大块内存中切个内存并不是真的直接切割,而是,将大块内存的头指针向后移动申请的大小位,之后不会将前面的空间分配给用户。

  • 由于链表直接使用分割的内存块来保存下一个内存块的地址,如果申请的内存块小于指针字节数怎么处理?

        做一个判断,当申请的内存块大小于当前平台的指针大小,直接向上取整,切割的内存块大小为指针大小。

	//可能切割的内存块小于指针大小
	static  size_t GetSize(){
		if (sizeof(T) < sizeof(void*)){
			return sizeof(void*);
		}
		else{
			return sizeof(T);
		}
	}
  • 如何来让链表结点内存块来保存下一个内存块的地址?

        利用强转和解引用

//32位平台下,强转成int*再解引用,得到4字节空间
*(int *)obj = freelist;

//64位平台下,强转成long long*,再解引用,得到8字节
*(long long*) obj = freelist; 
  • 32为平台下指针大小4字节,64位平台下指针大小8字节,链表内存块中如何分配空间来保存下一个内存块的地址?
//1.通过判断
if(sizeof(void*) == 4){
{
    //32位平台下,强转成int*再解引用,得到4字节空间
    *(int *)obj = freelist;

}
else{
    //64位平台下,强转成long long*,再解引用,得到8字节
    *(long long*) obj = freelist;     
}

//2.直接得到指针大小空间,解引用时void*,直接是指针大小
*(void**)obj = freelist;
  • 如何销毁定长内存池?

        将大块内存池用链表连接起来,保存好大块内存的起始地址。销毁时,只需要直接遍历该链表,释放空间即可。这就是为什么要将申请的大块空间设置成结构体的原因。

struct Memory{
	Memory(){
		leftsize = 1024 * 1;
		memory = (char *)malloc(leftsize);
		std::cout << (void *)memory << std::endl;
		head = memory;
	}

	char *memory = nullptr;//内存池,大块内存,char*步长一字节,方便分配
	Memory *next = nullptr;//指向下一个内存池
	size_t leftsize = 0;//剩余空间
	char *head = nullptr;//保存内存块起始地址,方便释放
	//析构函数
	~Memory(){
		free(head);
	}
};

    void Destroy(){
		while (m){
			Memory *tmp = m;
			m = m->next;
			delete tmp;//调用结构体析构函数,销毁空间
			
		}
	}

        1.3 代码

#pragma once

#include "Comment.h"


struct Memory{
	Memory(){
		leftsize = 1024 * 1;
		memory = (char *)malloc(leftsize);
		std::cout << (void *)memory << std::endl;
		head = memory;
	}

	char *memory = nullptr;//内存池,大块内存,char*步长一字节,方便分配
	Memory *next = nullptr;//指向下一个内存池
	size_t leftsize = 0;//剩余空间
	char *head = nullptr;//保存内存块起始地址,方便释放
	~Memory(){
		free(head);
	}
};

template<class T>
class ObjectPool{
private:
	//将大的内存块连接起来,方便释放
	Memory *m = nullptr;
	void *freelist = nullptr;//自由链表头指针,保存用户使用完的空间

	//可能切割的内存块小于指针大小
	static  size_t GetSize(){
		if (sizeof(T) < sizeof(void*)){
			return sizeof(void*);
		}
		else{
			return sizeof(T);
		}
	}
	//指针也可以不需要引用
	static void*& GetIndex(T*& obj){
		return (*(void **)obj);
	}

	void Destroy(){
		while (m){
			Memory *tmp = m;
			m = m->next;
			delete tmp;
			
		}
	}

public:
	T* New(){
		//首先去看链表中有没有
		//没有去内存池中切
		T* obj = nullptr;
		if (freelist){
			obj = (T*)freelist;
			//链表的下一个位置
			freelist = GetIndex(obj);
			return obj;
		}
		else{
			//可能没有内存池,需要去申请或者空间不够
			if (m == nullptr || m->leftsize < sizeof(T)){
				//连接起来
				Memory *tmp = new Memory();
				tmp->next = m;
				m = tmp;
			}
			
			obj = (T*)m->memory;
			m->memory += GetSize();
			m->leftsize -= GetSize();
			//定位new,再obj处开辟空间,并且可以调用构造函数
			new(obj)T;
			return obj;


		}


	}
	void Delete(T* obj){
		//调用析构
		obj->~T();
		//不能free,申请大块空间,不能free部分空间
		//直接用切割的内存保存子下一个链表的地址
		//强转成void**,再解引用,防止不同平台下指针字节数不同
		//头插
		GetIndex(obj) = freelist;
		freelist = obj;
	}

	~ObjectPool(){
		Destroy();
	}


};

以上是关于简单实现定长内存池的主要内容,如果未能解决你的问题,请参考以下文章

简单内存池与定长内存池

开胃菜-定长内存池

开胃菜-定长内存池

开胃菜-定长内存池

高并发内存池——基于Google开源项目tcmalloc的简洁实现

高并发内存池——基于Google开源项目tcmalloc的简洁实现