C++内存池(附源码)

C++内存池(附源码)

  前段时间阅读了Nginx的源码,其对内存高效的管理给我留下了深刻的印象,而内存管理的核心便是内存池。于是想自己实现一个C++版本的内存池,这方面当然还是STL的内存池最为经典,所以免不了参悟借鉴。内存池的概念早已经是老生常谈,然而把内存池实现的高效安全仍是个比较艰巨的问题。内存池的原理简单来讲就是一次性的向系统申请大量的内存,之后再有内存请求的时候,如果内存池的内存大小能够满足请求,就从内存池里分配,不必再进行系统调用,从而实现性能提升,而多次的内存申请系统调用,很容易生成内存碎片而造成内存浪费。池的概念大体如此,线程池,进程池无出其右。内存池的实现主要解决的问题有:
    1. 内存池的块管理
    2. 内存的分配和回收
    3. 大块内存的分配和回收
    4. 对象初始化

内存池的块管理

   这方面可以直接参考STL的分配器的实现,SGI STL在进行内存分配时,默认使用了一个内存池。这个内存池的内存块从8Byte开始,每递增8Byte都生成一系列链表管理的内存块,一直到128Byte结束。内存块定义为:

    union MemNode {
		MemNode*	_next;
		char		_data[1];
	};

  union每个成员的起始地址都是开头的位置,所以每次仅能使用一个成员,在链表中由_next指向下个内存块的地址,在分配内存时由_data指向内存首地址,长度为1 的数组放在结构体最后一个成员位置,可以访问给结构体多分配的地址空间,这种技术叫做柔性数组。这样做的好处减少了对内存块管理时额外的内存损耗。想想我们学习数据结构时实现的链表,都是通过结构体的一个成员来指向下个节点的地址,多出了一个指针4Byte的内存消耗。参考STL,我们内存块的管理如下图所示:
内存块示意图

  有同学要问了,那我要是申请比128更大的内存怎么办?SGI 这里就直接走正常的内存申请,还是会有系统调用产生。因为系统对于程序请求的内存,管理时也会生成额外的内存控制数据占用内存,这样申请的内存越小,额外占用的内存比例就越高。 我们每次申请指定量的内存,然后将内存格式化到块管理的数组链表中。

    char* res;
	size_t need_bytes = size * nums;
	size_t left_bytes = _pool_end - _pool_start;

	//内存池够用
	if (left_bytes >= need_bytes) {
		res = _pool_start;
		_pool_start += need_bytes;
		return res;
	
	} else if (left_bytes >= size) {
		nums = left_bytes / size;
		need_bytes = size * nums;
		res = _pool_start;
		_pool_start += need_bytes;
		return res;

	} 
	size_t bytes_to_get = size * nums;

	if (!is_large) {
		if (left_bytes > 0) {
			MemNode* my_free = _free_list[FreeListIndex(left_bytes)];
			((MemNode*)_pool_start)->_next = my_free;
			_free_list[FreeListIndex(size)] = (MemNode*)_pool_start;
		}

	} else {
		free(_pool_start);
	}
	

	_pool_start = (char*)malloc(bytes_to_get);
	
	//内存分配失败
	if (0 == _pool_start) {
		throw std::exception("There memary is not enough!");
	}

	_malloc_vec.push_back(_pool_start);
	_pool_end = _pool_start + bytes_to_get;
	return ChunkAlloc(size, nums, is_large);

  将返回的内存添加到块管理队列中

    my_free = &(_free_list[FreeListIndex(size)]);

	*my_free = next = (MemNode*)(chunk + size);
	for (int i = 1;; i++) {
		current = next;
		next = (MemNode*)((char*)next + size);
		if (nums - 1 == i) {
			current->_next = nullptr;
			break;
			
		} else {
				current->_next = next;
			}
		}
内存的分配和回收

  每次从系统申请内存时都通过一个辅助函数将内存增到为8的倍数,上层请求内存时寻找最小能容纳当前请求的头节点索引

   //获取size最小8的倍数
   size_t RoundUp(size_t size) {
		return ((size + __align - 1) & ~(__align - 1));
	}
	//获取容纳当前size的最小内存块索引
	size_t FreeListIndex(size_t size) {
		return (size + __align - 1) / __align - 1;
	}

  当找到索引位置时,如果内存块不为空,则取出当前内存块,将之后的链表节点向前移动,如果内存不够的话,再次向系统请求新的内存。

    std::unique_lock<std::mutex> lock(_mutex);
	MemNode** my_free = &(_free_list[FreeListIndex(sz)]);
	MemNode* result = *my_free;
	if (result == nullptr) {
		void* bytes = ReFill(RoundUp(sz));
		memset(bytes, 0, sz);
		return bytes;
	}

	*my_free = result->_next;
	memset(result, 0, sz);
	return result;

  内存回收时与此理相同,通过辅助函数找到索引位置,将内存块放入首部位置,之前的内存块后移。

    MemNode* node = (MemNode*)m;
	MemNode** my_free = &(_free_list[FreeListIndex(len)]);

	std::unique_lock<std::mutex> lock(_mutex);
	node->_next = *my_free;
	*my_free = node;
	m = nullptr;
大块内存的分配和回收

  通过以上的内存管理,我们足以解决小块内存的非配和回收,但是还可能存在另一种需求,类似Nginx内存池有大块内存的管理,我们在实际开发中也会用到诸如接收发送缓存之类的需求。这里添加增加一个新的类CBlockMemaryPool,以管理大块的内存块,而且要支持动态的增减:
  通过一个vector来管理池中空闲的内存块,需要注意的是析构时要将所有从池中申请的内存还给内存池,不然就需要自己手动释放。申请内存块的生命周期管理可以交给智能指针

对象初始化

  与C语言实现内存池的不同之处在于,C语言可以只负责内存的分配而不用管内部数据的初始化,因为C语言没有对象的概念。但是在C++中,我们不仅仅要负责内存的分配,还要调用构造函数负责对象的初始化。大家知道C++中的 new操作符,一是负责内存申请,二是调用构造函数实现对象初始化。而C++中可以通过可变模板参数来实现任意数量任意参数的函数转发,再辅之std::forward完美转发,即可实现构造函数的调用功能。所以我实现的内存池对外提供内存申请的接口有三个:

        //for object. invocation of constructors and destructors
        template<typename T, typename... Args >
        T* PoolNew(Args&&... args);
        template<typename T>
        void PoolDelete(T* &c);
    
        //for continuous memory
        template<typename T>
        T* PoolMalloc(int size);
        template<typename T>
        void PoolFree(T* &m, int len);
    
        //for bulk memory. 
        //return one bulk memory node
        template<typename T>
        T* PoolLargeMalloc();
        template<typename T>
        void PoolLargeFree(T* &m);

  这样每次请求和释放都需要调用接口,C++对这种操作最熟悉不过,我们交给智能指针来管理即可。还有这里为什么没有重载new操作符来呢?因为new和delete的重载函数只能是static函数(因为new对象的时候,对象还没有创建),所以内存池的api通过重载new 和delete 实现,看起来很美好,但实际上是行不通的。我们要创建内存池的对象,每个内存池的对象管理的都是不同的内存。下面看下 PoolNew 调用构造函数的过程。

template<typename T, typename... Args>
T* CMemaryPool::PoolNew(Args&&... args) {
	int sz = sizeof(T);
	if (sz > __max_bytes) {
		void* bytes = malloc(sz);
		T* res = new(bytes) T(std::forward<Args>(args)...);
	return res;
	}

	std::unique_lock<std::mutex> lock(_mutex);
	MemNode** my_free = &(_free_list[FreeListIndex(sz)]);
	MemNode* result = *my_free;
	if (result == nullptr) {
		void* bytes = ReFill(RoundUp(sz));
		T* res = new(bytes) T(std::forward<Args>(args)...);
		return res;
	}
	*my_free = result->_next;
	T* res = new(result) T(std::forward<Args>(args)...);
	return res;
}

  到这里基本上所有的功能都已经实现完毕。但是既然我们支持创建内存池的对象,那什么时候释放内存池占有的内存呢? 当然是析构函数中! 但是怎么释放呢? 我们是通过malloc 库函数申请的内存,释放的时候自然是去调用free释放。但是我们不能通过循环块的数组和链表去释放内存。因为我们申请的时候是一整块去申请的,释放的时候只要通过每次申请的头地址去释放即可。所以我在这里添加了一个辅助的std::vector来存储每次申请内存的地址,释放的时候只遍历这个std::vector即可。

	//声明
	std::vector<char*>  _malloc_vec;

	//存储
	_pool_start = (char*)malloc(bytes_to_get);
	if (0 == _pool_start) {
		throw std::exception("There memary is not enough!");
	}
	_malloc_vec.push_back(_pool_start);
	
	//释放
	for (auto iter = _malloc_vec.begin(); iter != _malloc_vec.end(); ++iter) {
		if (*iter) {
			free(*iter);
		}
	}

  一些线程安全相关的内容没有在文章里提到,以上代码还没有经过充分的测试,如有错误的地方欢迎大家指出。

  以下是使用实例:

class test1 {
public:
	int aaaa;
	int bbbb;
	int cccc;
	int dddd;

	explicit test1(int a, int b, int c, int d):aaaa(a), bbbb(b), cccc(c), dddd(d){
		std::cout << "test1()" << std::endl;
	}
	~test1() {
		std::cout << "~test1()" << std::endl;
	}
};

class test2 {
public:
	int aaaa;

	test2() {
		std::cout << "test2" << std::endl;
	}
	~test2() {
		std::cout << "~test2()" << std::endl;
	}
};

int main() {
	CMemaryPool pool;
	test1* t1 = pool.PoolNew<test1>(1,2,3,4);
	t1->aaaa = 1000;
	t1->bbbb = 1000;
	t1->cccc = 1000;
	t1->dddd = 1000;
	pool.PoolDelete<test1>(t1);

	test2* t2 = pool.PoolNew<test2>();
	t2->aaaa = 1000;

	pool.PoolDelete<test1>(t1);
	pool.PoolDelete<test2>(t2);

	int len1 = sizeof(unsigned long);
	int len2 = sizeof(char*);

	int a;
	std::cin >> a;
}

  最后贴出源码地址
  GitHub : https://github.com/caozhiyi/CppNet/blob/master/base/MemoryPool.h

  • 4
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值