C++如何实现定长内存池详解

目录
  • 1. 池化技术
  • 2. 内存池概念
    • 2.1 内存碎片
  • 3. 实现定长内存池
    • 3.1 定位new表达式(placement-new)
    • 3.2 完整实现
  • 总结

1. 池化技术

池是在计算机技术中经常使用的一种设计模式,其内涵在于:将程序中需要经常使用的核心资源先申请出来,放到一个池内,由程序自己管理,这样可以提高资源的使用效率,也可以保证本程序占有的资源数量。 经常使用的池技术包括内存池、线程池和连接池(数据库经常使用到)等,其中尤以内存池和线程池使用最多。

2. 内存池概念

内存池(Memory Pool) 是一种动态内存分配与管理技术。 通常情况下,程序员习惯直接使用 new、delete、malloc、free 等API申请分配和释放内存,这样导致的后果是:当程序长时间运行时,由于所申请内存块的大小不定,频繁使用时会造成大量的内存碎片从而降低程序和操作系统的性能。内存池则是在真正使用内存之前,先申请分配一大块内存(内存池)留作备用,当程序员申请内存时,从池中取出一块动态分配,当程序员释放内存时,将释放的内存再放入池内,再次申请池可以 再取出来使用,并尽量与周边的空闲内存块合并。若内存池不够时,则自动扩大内存池,从操作系统中申请更大的内存池。

2.1 内存碎片

  • 内碎片:

内部碎片就是已经被分配出去(能明确指出属于哪个进程)却不能被利用的内存空间;内部碎片是处于区域内部或页面内部的存储块。占有这些区域或页面的进程并不使用这个 存储块。而在进程占有这块存储块时,系统无法利用它。直到进程释放它,或进程结束时,系统才有可能利用这个存储块。(编译器会对数据进行对齐操作,当不是编译器的最小对齐数的整数倍的时候需要添加一些来保证对齐,那么这块为了对齐而添加的就是内碎片)

  • 外碎片(通常所讲的内存碎片):

假设系统依次分配了16byte、8byte、16byte、4byte,还剩余8byte未分配。这时要分配一个24byte的空间,操作系统回收了一个上面的两个16byte,总的剩余空间有40byte,但是却不能分配出一个连续24byte的空间,这就是外碎片问题。(本来有足够的内存,但是由于碎片化无法申请到稍大一些的连续内存)

3. 实现定长内存池

3.1 定位new表达式(placement-new)

定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。使用格式:new (place_address) type或者new (place_address) type(initializer-list),place_address必须是一个指针,initializer-list是类型的初始化列表。
使用场景:

定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要**使用new的定义表达式进行显示调构造函数**进行初始化。

3.2 完整实现

即实现一个 FreeList,每个 FreeList 用于分配固定大小的内存块,比如用于分配 32字节对象的固定内存分配器,之类的。

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

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

实现的思想:

  1. 先向内存申请一块大的内存,如果需要,那么就对这块已经申请出来的内存进行切割(减少了和操作系统底层打交道的次数,效率也就提高了,内存池一定是可以解决申请和释放内存的效率的)
  2. 对于不需要的小块内存,并不是将其进行释放掉,而是使用一个freeList将他们管理起来,如果freeList中有了空余的,那么再次申请内存首先会到自由链表中取,而不是去申请出来的大内存块进行切割
  3. 对于这个申请出来的小块内存,前4个或者8个字节存放的是下一个小内存块的地址(这是由于在32位平台下指针的大小是4字节,在64位平台下指针则是8字节),这里如何巧妙的进行平台下指针大小的适配,需要好好的进行琢磨。
  4. (帮助理解3)指针就是地址,那么指针的类型是为了解引用取到大小,对于所申请出来的内存的类型我是不关心的,在32位平台下我就想取出他的前4个字节,然后存放我的下一个小内存的地址,所以把obj强转为int*类型,然后在解引用就可以拿到前4个字节。那如果在64位平台下,就应该取其前8个字节来存放下一个小内存的地址,但是如果都写为取前4个字节的话,这里就会发生指针越界的问题。下述代码所写的Nextobj()接口函数就是为了能够取出小内存中的前4个字节或者8个字节。我需要的类型是void*,可以自动的适配平台(类比于上述的int类型,就可以相通)
//实现一个定长的内存池(针对某一个具体的对象,所以起名字叫ObjictPool)
#pragma once 

#include"Common.h"

template<class T>
class ObjectPool
{
public:
	~ObjectPool()
	{
		//
	}
	//此时代码还存一个很大的问题:我们默认这里取的是前四个字节,但是在64位的平台下,需要取的应该是这块小内存的前8个字节来保存地址
	void*& Nextobj(void* obj)
	{
		return *((void**)obj); //对于返回的void*可以自动的适配平台
	}
	//申请内存的函数接口
	T* New()
	{
		T* obj = nullptr;
		//一上来首先应该判断freeList
		if (_freeList)
		{
			//那就直接从自由链表中取一块出来
			obj = (T*)_freeList;
			//_freeList = (void*)(*(int*)_freeList);
			_freeList = Nextobj(_freeList);
		}
		else
		{
			//表示自由链表是空的
			//那么这里又要进行判断,memory有没有
			if (_leftSize < sizeof(T)) //说明此时空间不够了
			{
				//那么就进行切割
				_leftSize = 1024 * 100;
				_memory = (char*)malloc(_leftSize);
				//对于C++来说,如果向系统申请失败了,则会抛异常
				if (_memory == nullptr)
				{
					throw std::bad_alloc();
				}
			}
			//进行memory的切割
			obj = (T*)_memory;
			_memory += sizeof(T); //这里如果想不通可以画一下图,很简单
			_leftSize -= sizeof(T); //表示剩余空间的大小
		}
		new(obj)T;  //定位new,因为刚申请的空间内如果是自定义类型是没有初始化的
		//所以需要可以显示的调用这个类型的构造函数,这个是专门配合内存池使用的
		return obj;
	}

	void Delete(T* obj)
	{
		obj->~T();//先把自定义类型进行析构
		//然后在进行释放,但是此时还回来的都是一块一块的小内存,无法做到一次性进行free,所以需要一个自由链表将这些小内存都挂接住
		//这里其实才是核心的关键点
		//对于指针来说,在32位的平台下面是4字节,在64位平台下面是8字节

		//头插到freeList
		//*((int*)obj)= (int)_freeList;
		Nextobj(obj) = _freeList;
		_freeList = obj;
	}
private:
	char* _memory = nullptr;//这里给char*是为了好走大小,并不是一定要给T*或者void*
	int _leftSize = 0; //为什么会加入这个成员变量呢?因为你的menory += sizeof(T),有可能就会造成越界的问题
	void* _freeList = nullptr; //给一些缺省值,让他的构造函数自己生成就可以了
};

struct TreeNode
{
	int _val;
	TreeNode* _left;
	TreeNode* _right;

	TreeNode()
		:_val(0)
		, _left(nullptr)
		, _right(nullptr)
	{}
};
void TestObjectPool()
{

	验证还回来的内存是否重复利用的问题
	ObjectPool<TreeNode> tnPool;
	TreeNode* node1 = tnPool.New();
	TreeNode* node2 = tnPool.New();
	cout << node1 << endl;
	cout << node2 << endl;

	tnPool.Delete(node1);
	TreeNode* node3 = tnPool.New();
	cout << node3 << endl;

	cout << endl;

	//验证内存池到底快不快,有没有做到性能的优化
	//new底层本身调用的malloc,会一直和操作系统的底部打交道
	size_t begin1 = clock();
	std::vector<TreeNode*> v1;
	for (int i = 0; i < 1000000; ++i)
	{
		v1.push_back(new TreeNode);
	}
	for (int i = 0; i < 1000000; ++i)
	{
		delete v1[i];
	}
	size_t end1 = clock();

	//这里我们调用自己所写的内存池
	ObjectPool<TreeNode> tnPool;
	size_t begin2 = clock();
	std::vector<TreeNode*> v2;
	for (int i = 0; i < 1000000; ++i)
	{
		v2.push_back(tnPool.New());
	}
	for (int i = 0; i < 1000000; ++i)
	{
		tnPool.Delete(v2[i]);
	}
	size_t end2 = clock();

	cout << end1 - begin1 << endl;
	cout << end2 - begin2 << endl;
}

这个定长的内存池依旧存在着大量的问题:

  1. 我们所采用的是取这块小内存的前4个或者8个字节来存放下一个小内存的地址,但是如果这里的模板类型T是一个char类型怎么办,它本身都没有4字节,怎么来存放?(解决的办法就是,进行一次判断如果sizeof(T) < sizeof(T*)的大小,那么就开辟T*的大小)
  2. 无法编写这个ObjectPool的析构函数,因为申请的都是一个个的小块内存,但是对于free来说,应该是一次性的对整个所开辟出来的内存块都进行释放(解决的办法就是,将这些向操作系统申请的大块内存也管理起来,如果小块内存都还回来了,那么就可以对这个大块内存进行释放)

对于上述的具体实现可以参考下面这篇文章写的很详细:

如何设计一个简单内存池

总结

到此这篇关于C++如何实现定长内存池的文章就介绍到这了,更多相关C++定长内存池内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C++内存池两种方案解析

    目录 C++内存池 1.C++内存池分析 2.多此一举方案 3.分时复用改进方案 4.其他的思考 C++内存池 前言: 使用new expression为类的多个实例分配动态内存时,cookie导致内存利用率可能不高,此时我们通过实现类的内存池来降低overhead.从不成熟到巧妙优化的内存池,得益于union的分时复用特性,内存利用率得到了提高. 1.C++内存池分析 在实例化某个类的对象时(在heap而不是stack中),若不使用array new,则每次实例化时都要调用一次内存分配函数,类

  • C++内存池的简单实现

    目录 一.内存池基础知识 1.什么是内存池 1.1 池化技术 1.2 内存池 2.内存池的作用 2.1 效率问题 2.2 内存碎片 3.内存池技术的演进 二.简易内存池原理 1.整体设计 1.1 内存池结构 1.2 申请内存 1.3 释放内存 2.详细剖析 2.1 blockNode结构 2.2 单个对象的大小 3.性能比较 三.简易内存池完整源码 一.内存池基础知识 1.什么是内存池 1.1 池化技术 池化技术是计算机中的一种设计模式,主要是指:将程序中经常要使用的计算机资源预先申请出来,由程

  • C++手写内存池的案例详解

    引言 使用new expression为类的多个实例分配动态内存时,cookie导致内存利用率可能不高,此时我们通过实现类的内存池来降低overhead.从不成熟到巧妙优化的内存池,得益于union的分时复用特性,内存利用率得到了提高. 原因 在实例化某个类的对象时(在heap而不是stack中),若不使用array new,则每次实例化时都要调用一次内存分配函数,类的每个实例在内存中都有上下两个cookie,从而降低了内存的利用率.然而,array new也有先天的缺陷,即只能调用默认无参构造

  • C++如何实现定长内存池详解

    目录 1. 池化技术 2. 内存池概念 2.1 内存碎片 3. 实现定长内存池 3.1 定位new表达式(placement-new) 3.2 完整实现 总结 1. 池化技术 池是在计算机技术中经常使用的一种设计模式,其内涵在于:将程序中需要经常使用的核心资源先申请出来,放到一个池内,由程序自己管理,这样可以提高资源的使用效率,也可以保证本程序占有的资源数量. 经常使用的池技术包括内存池.线程池和连接池(数据库经常使用到)等,其中尤以内存池和线程池使用最多. 2. 内存池概念 内存池(Memor

  • 构建高效的python requests长连接池详解

    前文: 最近在搞全网的CDN刷新系统,在性能调优时遇到了requests长连接的一个问题,以前关注过长连接太多造成浪费的问题,但因为系统都是分布式扩展的,针对这种各别问题就懒得改动了. 现在开发的缓存刷新系统,对于性能还是有些敏感的,我后面会给出最优的http长连接池构建方式. 老生常谈: python下的httpclient库哪个最好用? 我想大多数人还是会选择requests库的.原因么?也就是简单,易用! 如何蛋疼的构建reqeusts的短连接请求: python requests库默认就

  • 基于一个简单定长内存池的实现方法详解

    主要分为 3 个部分,memoryPool 是管理内存池类,block 表示内存块,chunk 表示每个存储小块.它们之间的关系为,memoryPool 中有一个指针指向某一起始 block,block 之前通过 next 指针构成链表结构的连接,每个 block 包含指定数量的 chunk.每次分配内存的时候,分配 chunk 中的数据地址. 主要数据结构设计: Block: 复制代码 代码如下: struct block {    block * next;//指向下一个block指针   

  • 线程池之newFixedThreadPool定长线程池的实例

    newFixedThreadPool定长线程池的实例 newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待.newFixedThreadPool固定线程池, 使用完毕必须手动关闭线程池, 否则会一直在内存中存在. 示例代码: public class ThreadPoolFixed { public static void main(String[] args) { //设置线程池大小为3 ExecutorService fixedThread

  • Java 数据库连接池详解及简单实例

    Java 数据库连接池详解 数据库连接池的原理是: 连接池基本的思想是在系统初始化的时候,将数据库连接作为对象存储在内存中,当用户需要访问数据库时,并非建立一个新的连接,而是从连接池中取出一个已建立的空闲连接对象.使用完毕后,用户也并非将连接关闭,而是将连接放回连接池中,以供下一个请求访问使用.而连接的建立.断开都由连接池自身来管理.同时,还可以通过设置连接池的参数来控制连接池中的初始连接数.连接的上下限数以及每个连接的最大使用次数.最大空闲时间等等.也可以通过其自身的管理机制来监视数据库连接的

  • 基于tomcat的连接数与线程池详解

    前言 在使用tomcat时,经常会遇到连接数.线程数之类的配置问题,要真正理解这些概念,必须先了解Tomcat的连接器(Connector). 在前面的文章 详解Tomcat配置文件server.xml 中写到过:Connector的主要功能,是接收连接请求,创建Request和Response对象用于和请求端交换数据:然后分配线程让Engine(也就是Servlet容器)来处理这个请求,并把产生的Request和Response对象传给Engine.当Engine处理完请求后,也会通过Conn

  • C语言变长数组使用详解

    看如下代码: #include<stdio.h> typedef struct { int len; int array[]; }SoftArray; int main() { int len = 10; printf("The struct's size is %d\n",sizeof(SoftArray)); return 0; } 运行结果: [root@VM-0-7-centos mydoc]# ./a.out The struct's size is 4 我们可以

  • java线程池详解及代码介绍

    目录 一.线程池简介 二.四种常见的线程池详解 三.缓冲队列BlockingQueue和自定义线程池ThreadPoolExecutor 总结 一.线程池简介 线程池的概念 线程池就是首先创建一些线程,它们的集合称为线程池,使用线程池可以很好的提高性能,线程池在系统启动时既创建大量空闲的线程,程序将一个任务传给线程池.线程池就会启动一条线程来执行这个任务,执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务. 线程池的工作机制 在线程池的编程模式下,任务是提交给整个

  • Java 常量池详解之字符串常量池实现代码

    目录 1.字符串常量池(String Constant Pool) 1.1:字符串常量池在Java内存区域的哪个位置? 1.2:字符串常量池是什么? 1.3 字符串常量池生成的时机? 如何将String对象放入到常量池 String 对象代码案例解析 new string(“abc”)创建了几个对象 解析public native String intern() 方法 Integer 对象代码案例解析 为啥Integer i1 =10 跟Integer.valueOf(10) 是相等的? 为啥I

  • Springboot 配置线程池创建线程及配置 @Async 异步操作线程池详解

    目录 前言 一.创建一个Springboot Web项目 二.新建ThreadPoolConfig 三.新建controller测试 四.演示结果 前言 众所周知,创建显示线程和直接使用未配置的线程池创建线程,都会被阿里的大佬给diss,所以我们要规范的创建线程. 至于 @Async 异步任务的用处是不想等待方法执行完就返回结果,提高软件前台响应速度,一个程序中会用到很多异步方法,所以需要使用线程池管理,防止影响性能. 一.创建一个Springboot Web项目 需要一个Springboot项

随机推荐