初探C++内存池项目 ---(二)内存池的实现及原理详解

一.内存池介绍

为了丰富内容,我在把内存池介绍一遍~

内存池是池化技术中的一种形式。通常我们在编写程序的时候回使用 new delete 这些关键字来向操作系统申请内存,而这样造成的后果就是每次申请内存和释放内存的时候,都需要和操作系统的系统调用打交道,从堆中分配所需的内存。如果这样的操作太过频繁,就会找成大量的内存碎片进而降低内存的分配性能,甚至出现内存分配失败的情况。
而内存池就是为了解决这个问题而产生的一种技术。从内存分配的概念上看,内存申请无非就是向内存分配方索要一个指针,当向操作系统申请内存时,操作系统需要进行复杂的内存管理调度之后,才能正确的分配出一个相应的指针。而这个分配的过程中,我们还面临着分配失败的风险。
所以,每一次进行内存分配,就会消耗一次分配内存的时间,设这个时间为 T,那么进行 n 次分配总共消耗的时间就是 nT;如果我们一开始就确定好我们可能需要多少内存,那么在最初的时候就分配好这样的一块内存区域,当我们需要内存的时候,直接从这块已经分配好的内存中使用即可,那么总共需要的分配时间仅仅只有 T。当 n 越大时,节约的时间就越多。

二.项目源码及分析

源码地址:
https://github.com/82457097/Linux/tree/master/MemoryPool

1.MemoryPool

#ifndef MEMORY_POOL_H
#define MEMORY_POOL_H

#include<limits>
#include<cstddef>

template<typename T,size_t BlockSize = 4096>
class MemoryPool {
public:
	typedef T* pointer;

	//定义rebind<U>::other 的接口
	//other的作用就是把模板的类型从T转成U
	//你可以这样使用 MemoryPoll<T>::rebind<U>::other
	//给其取别名的时候需要注意,详细可以看上一篇文章中的说明,格式如下
	//typedef typename MemoryPool<T>::template rebind<U>::other XXXX
	template<typename U>
	struct rebind {
		typedef MemoryPool<U> other;
	};	
	
	//构造函数
	MemoryPool() {
		m_currentBlock = nullptr;
		m_currentSlot = nullptr;
		m_lastSlot = nullptr;
		m_freeSlots = nullptr;
	}
	
	//析构函数
	~MemoryPool() {
		//创建一个指针 指向当前已分配出去的内存区块链
		m_slotPointer curr = m_currentBlock;
		//循环删除 reinterpret_cast 为强制类型转换符
		while(curr != nullptr) {
			m_slotPointer temp = curr->next;
			operator delete(reinterpret_cast<void>(curr));
			curr = temp;
		}
	}
	
	//给新建对象分配内存节点,这一块代码比较多,但是逻辑不难,也是内存池比较重要的业务
	pointer allocate(size_t n = 1, const pointer hint = 0) {
		//如果有空闲节点,那么直接分配出去
		if(m_freeSlots != nullptr) {
			pointer ret = reinterpret_cast<pointer>(m_freeSlots);
			m_freeSlots = m_freeSlot->next;
			return ret;
		}
		else {
			//如果节点使用完了,则分配一个新的内存区块
			if(m_currentSlot >= m_lastSlot) {
				//分配一个新的内存块,并指向前一个内存区块,逻辑就是头插法给链表增加一个节点
				m_dataPointer newBlock = reinterpret_cast<m_dataPointer>(operator new(BlockSize));
				reinterpret_cast<m_slotPointer>(newBlock)->next = m_currentBlock;
				m_currentBlock = reinterpret_cast<m_slotPointer>(newBlock);
				//填补整个区块来满足元素内存区域的对齐要求;
				m_dataPointer body = newBlock + sizeof(m_slotPointer);
				//在64位的机器上,uintptr_t 是unsigned long int的别名;
				//在32位的机器上,uintptr_t 是unsigned int的别名。
				//为什么用这个,是为了指针转换的安全性考虑,提高程序的可移植性
				uintptr_t result = reinterpret_cast<uintptr_t>(body);
				//alignof 的作用是获取指定对象的字节对齐方式
				//这一部分是给各个节点功能指针确定位置,计算内存地址的偏移量
				size_t bodyPadding = (alignof(m_slotType) - result) % alignof(m_slotType);
				m_currentSlot = reinterpret_cast<m_slotPointer>(body + bodyPadding);
				m_lastSlot = reinterpret_cast<m_slotPointer>(newBlock + BlockSize - sizeof(m_slotType));
			}
			return reinterpret_cast<Pointer>(m_currentSlot++);
		}
	}
	//销毁指针p所指的节点
	void deallocate(pointer p, size_t n = 1) {
		if(p != nullptr) {
			//reinterpret_cast是强制类型转换符
			//要访问next必须强制将p转成m_slotPointer
			//实际上就是将需要销毁的内存节点用头插法插入m_freeSlots空闲空间链
			reinterpret_cast<m_slotPointer>(p)->next = m_freeSlots;
			m_freeSlots = reinterpret_cast<m_slotPointer>(p);
		}
	}
	
	//调用构造函数,使用std::forward 转化变参模板
	//详细说明见下文连接
	template<typename U, typename... Args>
	void construct(U* p, Args&&... args) {
		new (p) U (std::forward<Args>(args)...);
	}

	//销毁内存池中的对象,即调用对象的析构函数
	template<typename U>
	void destroy(U* p) {
		p->~U();
	}

private:
	//用于初始化内存中的节点
	union Slot {
		T data;
		Slot* next;
	};
	
	//数据节点指针
	typedef char* m_dataPointer;
	//对象
	typedef Slot m_slotType;
	//对象指针
	typedef Slot* m_slotPointer;

	//指向已分配的内存区块
	m_slotPointer m_currentBlock;
	//指向当前已分配内存区块的一个对象节点
	m_slotPointer m_currentSlot;
	//指向最后一个对象节点
	m_slotPointer m_lastSlot;
	//指向空闲节点
	m_slotPointer m_freeSlots;
	//断言内存池是否太小
	static_assert(BlockSize >= 2 * sizeof(m_slotType), "BlockSize too small.");
};

#endif

2.一些说明

这些都是项目的一些扩展点,不做详细解释,给大家提供我自己参考的文章链接~

  • 2.1 size_t的用法

size_t的用法及作用可以参考这篇文章

  • 2.2 可变参数模板的介绍

关于可变参数模板可以参考这篇文章

  • 2.3 关于uintptr_t可以参考一下这篇文章

关于uintptr_t的作用,可以参考这篇文章

三.项目总结

其实经过研究就会发现,内存其实就是通过链表将内存空间串起来,当然根据不同的需要会同时存在很多个不同功能的链表,比如我们实现的内存池,就有存放已分配对象的链表,串联大内存块的链表,存放空闲(或者已释放)内存链表;然后通过一些操作,实现内存的分配管理。
这个内存池分配机制其实和STL中的vector类似,不过vector容器好像申请内存块的大小是翻倍增加的,会导致一定的空间浪费,但是这样会节省时间,程序设计总是这样,空间换时间,时间换空间,根据自己的需要来就好了。
系统的内存分配我只了解一点,估计也是大同小异。系统的分配机制更为复杂一些,当你申请一块内存的时候,他会根据你申请的大小去空闲内存区块链表上去寻找大于你申请量的节点,然后将节点一分为二,把你需要的大小给你,剩下的再插入空闲链表。这样的话,频繁的分配会造成很多内存碎片,浪费时间,而且当你申请的内存量大于所有剩下的空闲节点时,他还会把内存碎片整合,再分配给你,是一个很复杂的过程,对时间的开销比较大。
所以通过对比不难发现,自己实现的内存池相对于系统来分配,其优势还是很大的,虽然最后都是向系统去申请内存,但是有内存池作为一个缓冲,会减少很多分配次数,节省系统去为你找寻合适空闲块的时间。
以上只是我这一段时间学习内存的一些拙见,不确保理解的很正确,也希望大家能给出指正或者宝贵意见!

发布了15 篇原创文章 · 获赞 5 · 访问量 1272

猜你喜欢

转载自blog.csdn.net/weixin_44816732/article/details/103899112