• 【C++项目】手动实现一个定长内存池(了解内存分配机制、定长内存提高效率 附源码)


    0000

    项目源代码:高并发内存池

    1.项目介绍

    在这里插入图片描述
    这是一个 C++ 中的对象池(Object Pool)的简单实现,用于更有效地管理对象的内存分配和回收。对象池是一种内存管理技术,旨在减少频繁分配和释放对象的开销,从而提高程序的性能。

    • 以下是该对象池的一些关键部分和功能:

    New() 函数:用于获取一个新的对象。它首先检查是否有已归还的内存块,如果有,则使用归还的内存,否则从内存池中分配一个新的对象。它使用 malloc 来分配内存,然后使用定位 new 来调用对象的构造函数,初始化对象。

    Delete() 函数:用于回收一个对象。它首先调用对象的析构函数以清理对象,然后将对象添加到内存池的归还链表中。为了解决不同对象类型的大小差异问题,它使用了一个二级指针 _freeList,这样不同类型的对象都可以通过 _freeList 连接在一起。

    内存分配:初始时,对象池会通过 malloc 分配一块大内存块,大小为1024 * 128字节。然后,通过不断更新 _memory 指针和 _remainBytes 记录内存块的剩余空间,来实现对象的内存分配。

    性能测试:在代码中,有一个测试函数 TestObjectPool,用于测试直接使用 new 和对象池分配的性能差异。它执行多轮的分配和释放操作,以测量两种方法的性能。

    这个对象池的实现是为了演示目的而创建的,可以在需要管理大量相同类型对象的情况下提高性能

    2.代码部分

    • ObjectPool.h
    #pragma once
    #include
    #include
    #include
    template <class T>
    class ObjectPool
    {
    public:
    	T* New()
    	{
    		T* obj = nullptr; //给一个对象指针 指向内存
    		if (_freeList) //如果有归还的内存先用归还内存
    		{
    			//next指向freelist+sizeof(T)的一个位置
    			void* next = static_cast<void*>(static_cast<char*>(_freeList) + sizeof(T));
    			//void* next = *((void**)_freeList);
    			obj = (T*)_freeList;
    			_freeList = next;
    			return obj;
    		}
    		else
    		{
    			if (remainBytes < sizeof(T))//如果剩余的内存不够 就新开一块空间
    			{
    
    				_memory = (char*)malloc(1024 * 128); //初次加载内存池创建内存
    				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;//更新remainBytes
    
    			//定位new 用于调用T的构造函数初始话
    			new(obj)	T;
    			return obj;
    		}
    
    	}
    
    	void Delete(T* obj)
    	{
    		//显示调用T的析构函数清理对象
    		obj->~T();
    
    		//这里有一个问题 :当T是char类型的时候 大小只有1字节
    			//在32位系统下 指针是4字节 64位下:8字节 
    			//_freeList = obj;    就会出现异常 所以要用一个二级指针解决这个问题
    		*(void**)obj = _freeList;
    		/*当使用二级指针(例如 int** 或 void**)时,它们只是指向另一个指针的指针。
    		这些指针的大小通常不取决于所指向的数据类型,而是固定的,
    		因此可以在不同位数的系统上正常工作*/
    		_freeList = obj;
    	}
    private:
    	char* _memory = nullptr;//指向大块内存块的地址
    	size_t remainBytes = 0;//大块内存剩余字节数
    	void* _freeList = nullptr;//归还回来的内存链表
    };
    
    
    //测试函数
    struct TreeNode
    {
    	int _val;
    	TreeNode* _left;
    	TreeNode* _right;
    
    	TreeNode()
    		:_val(0)
    		, _left(nullptr)
    		, _right(nullptr)
    	{}
    };
    
    
    void TestObjectPool()
    {
    	//申请释放的轮数
    	const size_t Rounds = 3;
    
    	//每轮申请释放的次数
    	const size_t N = 1000;
    
    
    	//直接new  测试
    	size_t begin1 = clock();
    	std::vector<TreeNode*>v1;
    	v1.reserve(N);//预留N个空间
    	for (size_t j = 0; j < Rounds; j++)
    	{
    		//插入N个元素
    		for (size_t i = 0; i < N; ++i)
    		{
    			v1.push_back(new TreeNode);//直接new
    		}
    		//删除N个元素
    		for (size_t k = 0; k < N; ++k)
    		{
    			delete v1[k];
    		}
    		v1.clear();//释放空间
    	}
    
    	size_t end1 = clock();
    	//用ObjectPool测试
    	ObjectPool<TreeNode>TNPool;
    	size_t begin2 = clock();
    	std::vector<TreeNode*>v2;
    	v2.reserve(N);
    	for (size_t j = 0; j < Rounds; j++)
    	{
    		//插入N个元素
    		for (size_t i = 0; i < N; ++i)
    		{
    			v2.push_back(TNPool.New());//用objectPool new
    		}
    		//删除N个元素
    		for (size_t k = 0; k < N; ++k)
    		{
    			TNPool.Delete(v2[k]);
    		}
    		v2.clear();//释放空间
    	}
    	size_t end2 = clock();
    
    	std::cout << "new const time:" << end1 - begin1 << std::endl;
    	std::cout << "objectPool const time:" << end2 - begin2 << std::endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • Test.c
    #define _CRT_SECURE_NO_WARNINGS
    #include"ObjectPool.h"
    int main()
    {
    	TestObjectPool();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    3.测试结果

    0001
    这里在处理大规模数据的时候效率差距会更明显一些,在这里测试中设置的规模Rounds和N的值设置得比较小(本人设备内存的原因),可以在自己的机器上设置,数据越大,效率差距越明显。

    0002
    0003

    4.相关细节分析总结

    • 注意点1

    0005
    在new()函数这里,这里所说的异常指的是回收内存链表的维护,因为这个链表freelist要存一个指针,
    0009
    如果这个数据类型是char类型的话大小只有1字节,而指针在32位系统下是4字节,32位系统下是8字节,所以会出现异常。这样处理的话就能巧妙避免这个问题。0010

    • 注意点2
      0006
      定位new 在处理自定义数据类型的时候很有必要

    定位 new:通过使用定位 new,可以在已分配的内存块上调用对象的构造函数,从而正确地初始化对象。这对于确保对象状态的一致性非常重要,尤其是在对象具有复杂的构造逻辑时。

    显式调用析构函数:在 Delete 函数中,通过显式调用对象的析构函数,您可以确保对象在释放之前正确地清理其资源。这对于防止资源泄漏和确保对象的正确析构非常重要。

    这两个操作一起确保对象池中的对象正确地管理其资源和生命周期。而不是仅仅释放内存,还确保对象的构造和析构逻辑得到执行。

    • 注意点3
      0007
      解决不同系统下数据类型不匹配问题

    最后创作不易,点赞支持爱你~请添加图片描述

  • 相关阅读:
    c++ this
    【Linux】UNIX 术语中,换页与交换的区别和Linux 术语中,换页与交换的区别?
    QT QGLWidge
    体育世界杂志体育世界杂志社体育世界编辑部2022年第4期目录
    【Arcpy】批量表格转xy点
    基于JAVA的图书借阅管理平台【数据库设计、源码、开题报告】
    餐饮商城外卖小程序的作用是什么
    Leetcode 84.柱状图中最大的矩形
    NR 5G RRC Setup Request
    测试计划包括哪些内容?
  • 原文地址:https://blog.csdn.net/weixin_62892290/article/details/133772407