[项目设计] 从零实现的高并发内存池(一)

🌈 博客个人主页Chris在Coding

🎥 本文所属专栏[高并发内存池]

❤️ 前置学习专栏[Linux学习]

 我们仍在旅途                     

目录

前言

        项目介绍

1.内存池

        1.1 什么是内存池

        池化技术

        内存池

        1.2 为什么需要内存池

        效率原因

        内存碎片问题

        1.3 实现定长内存池理解池化技术

        定长内存池的设计

        _freelist的设计

        New()和Delete()的设计

        与malloc()做性能对比


前言

        项目介绍

首先,本项目的原型是Google的一个开源项目TCMalloc,TCMalloc是谷歌开发的一种内存分配器(Memory Allocator),专门用于优化大规模的多线程应用程序的内存分配和管理。TCMalloc的全称是Thread-Caching Malloc,意为线程缓存内存分配器。它最初是为谷歌的服务器应用程序而开发的,旨在解决大规模多线程服务器应用程序中内存分配性能不佳的问题。

Google作为世界级大厂,学习并复现该项目, 我们能体会到其精妙的项目架构设计

1.内存池

        1.1 什么是内存池

池化技术

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

内存池

内存池(Memory Pool)是一种动态内存分配与管理技术,旨在优化内存的分配和释放过程,减少内存碎片化,提高程序和操作系统的性能。通常情况下,程序员直接使用诸如new、delete、malloc、free等API进行内存的申请和释放,但这种方式容易导致内存碎片化问题,尤其在长时间运行的情况下性能表现更为明显。
 

内存池的工作原理是在程序启动时预先申请一大块内存作为池,之后在程序运行过程中,从这个内存池中动态分配内存给程序使用。当程序需要内存时,可以直接从内存池中取出一个空闲的内存块;当程序释放内存时,将释放的内存块重新放回内存池中,以备后续使用。同时,内存池会尽可能地与周边的空闲内存块合并,以减少内存碎片的产生。如果内存池的空闲内存不足以满足程序需求,内存池会自动扩大,向操作系统申请更大的内存空间。

内存池的优点包括:

  • 减少内存碎片化:通过提前申请一大块内存并动态分配管理,内存池可以有效减少内存碎片的产生。
  • 提高性能:内存池避免了频繁的内存申请和释放操作,从而减少了系统开销,提高了程序和操作系统的性能。

        1.2 为什么需要内存池

效率原因

假设你是一个图书管理员,你管理着一个图书馆的书籍。每当有读者来借书时,你需要为他们分配一本书。如果每次有读者来借书时都去印刷新书(动态分配内存),等读者归还书籍后再销毁书籍(释放内存),那么你将花费大量的时间和精力来管理这个过程,而且频繁的印刷和销毁书籍会带来一定的成本和效率损失。

相反,如果你有一个固定数量的书库(内存池),这些书籍已经预先准备好并且处于待命状态。当读者来借书时,你只需要从书库中选择一本可用的书分配给他们,归还后书籍继续留在书库中等待下一个读者的到来。这样可以避免频繁的书籍印刷(内存分配)和销毁(内存释放),节省了时间和成本,提高了效率。

内存碎片问题

假设我们直接在堆上直接申请内存,依次申请出8ytes,24bytes,10bytes,20bytes,此时堆上还剩8bytes没被申请,而这时候我们向堆归还了原先的24bytes和20bytes的内存,这时如果我们想再去堆上申请一个30bytes的内存是申请不出来的,但是实际上我们此时空余的内存达到了52bytes,但可惜的是我们无法凑出连续的30bytes内存块.而这也就是所谓的外部内存碎片问题.

内存碎片问题通常被分为内部碎片问题和外部碎片问题。这两种碎片问题都是指在内存管理过程中未被有效利用的内存空间,但它们的来源和解决方法略有不同。

  • 内部碎片问题(Internal Fragmentation): 内部碎片是指已经被分配给进程或对象的内存空间中,由于未被完全利用而产生的空闲部分。这种情况通常出现在动态内存分配中,比如在分配内存时,由于分配的内存块大于所需大小,导致分配的内存中存在一些未被使用的空间。内部碎片问题通常由内存分配算法引起,例如,如果使用固定大小的内存块进行分配(如固定大小的内存池),而分配的内存大小与需求不匹配,就会产生内部碎片。

  • 外部碎片问题(External Fragmentation): 外部碎片是指未分配给任何进程或对象的内存空间中的零散碎片。这些碎片可能由已释放的内存块留下,但由于它们的大小和位置不符合当前内存分配需求,因此无法被有效利用。外部碎片问题通常由内存释放操作引起,释放的内存块使得内存空间出现了不连续的空闲区域,这些零散的空闲区域无法满足大块内存分配的需求,从而产生了外部碎片。

        1.3 实现定长内存池理解池化技术

malloc

在C/C++编程中,当我们需要动态分配内存时,确实不是直接与操作系统打交道,而是通过诸如malloc这样的函数来完成。malloc是一个C标准库提供的函数,它帮助我们从计算机系统的堆内存区域请求一定数量的连续内存空间。
在C++中,new关键字则是用来进行动态内存分配的另一种方式,它本质上是对malloc功能的扩展和封装,不仅分配内存,还能自动调用构造函数初始化对象(对于类类型数据)。当你使用new分配数组或对象时,它会确保每个元素或对象都被正确初始化。而delete关键字在释放内存的同时,也会调用析构函数来清理对象。

不同编译器和操作系统环境下,malloc的具体实现可能会有所不同。例如,在Windows下的Visual Studio编译器中,malloc由微软进行了特定的优化实现;而在Linux系统下,采用glibc库的编译器(如GCC)则使用ptmalloc作为默认的malloc实现。这些实现通常包含高级算法,旨在提高内存分配的效率、减少碎片并满足各种内存需求。

定长内存池的设计

malloc函数作为一个通用的内存分配器,它可以接受任意大小的内存请求,并试图在堆上找到合适的连续空间进行分配。然而,由于它的通用性,它必须处理各种尺寸的请求,这可能导致内部碎片(即分配出的内存块之间存在无法使用的空隙),并且每次分配或释放都需要一定的查找和维护成本,所以对于特定场景而言,其性能可能不如针对性设计的内存管理方案。


定长内存池(Fixed-size Memory Pool)正是这样一种针对性的设计,它适用于那些内存块大小固定的场景,例如在大量创建和销毁相同大小对象的应用中。在定长内存池中,内存预先按照固定大小进行划分,申请和释放内存的操作可以简化为从池中取出或归还一个预设大小的内存块,无需像malloc那样复杂的寻找合适大小的空间。这种做法能够显著减少内存分配和释放的开销,消除内部碎片。

通过从零实现一个定长内存池,我们可以更直接的理解池化技术,同时也是在为高并发内存池项目的实现做铺垫。

template<class T, long long Nums>
class ObjMemoryPool
{
public:
	ObjMemoryPool()
		:_freelist(nullptr), _pool(nullptr), _remainder(0)
	{}
	T* New()
	{

	}
	void Delete(T* obj)
	{
		
	}
private:
	void* _freelist;
	char* _pool;
	size_t _remainder;
};

我们首先写出定长内存池类ObjMemoryPool的大致框架,同时使用可变模板参数class T表示定长内存池存储的对象的类型和long long Nums来表示每次内存池预先申请多少个T大小的空间。

同时我们后面会通过一个_freelist来实现对回收对象的管理,_remainder则表示内存池剩余的字节数,同时我们会留出接口New(),Delete(),来调用以申请和销毁对象。

_freelist的设计

这里我们将_freelist(自由链表)设计成一个不带头的单向链表,把每个回收的对象当作链表的节点链接存储维护。

我们会从每一个对象的内存块中取出一块来存放下一个内存块的地址来实现指针链接

但是这个时候我们会遇到一个问题:指针的大小在32位和64位下的大小不一致,如何在32位平台取出4个字节,而在64位平台下取出8个字节呢?

void* nextobj = *(void**)obj;

这里通过将当前obj对象的指针强转为void**类型,即认为obj指向一个指针类型,这时候直接解引用该指针就一定是取出一个指针大小的内存块,我们就可以在其中读取或者写入下一个内存块的地址。

New()和Delete()的设计

由于是_freelist是单向链表,这里我们将节点的插入与取出都设计成O(1)时间复杂度的头插和头删。

Delete()

void Delete(T* obj)
{
	obj->~T();
	*(void**)obj = _freelist;
	_freelist = obj;
}

Delete()接口的实现最简单,我们只需要去调用内存块中对应类型的析构函数,再直接将其头插进_freelist里面即可。

 直接向堆申请空间

这里我们的项目将摆脱与malloc的联系,直接向堆申请空间使用和管理。

要想直接向堆申请内存空间,在Windows下,可以调用VirtualAlloc函数;在Linux下,可以调用brk或mmap函数。这里我在Windows下便选择使用VirtualAlloc函数。

#ifdef _WIN32
	#include <windows.h>
#endif

inline static void* SystemAlloc(size_t kpage)	{
#ifdef _WIN32
		void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#endif
		if (ptr == nullptr)
			throw std::bad_alloc();

		return ptr;
}
inline static void SystemFree(void* ptr)
{
#ifdef _WIN32
	VirtualFree(ptr, 0, MEM_RELEASE);
#endif
  • 0:此参数指定要分配的区域的起始地址。当设置为0时,系统会确定在哪里分配该区域。
  • kpage << 13:此参数指定要分配的内存大小,VirtualAlloc 函数返回的内存区域地址会在页边界上对齐,页大小通常是4KB(即2的12次方字节),这里就是指我们一次申请的内存块大小为kpage页
  • MEM_COMMIT表示根据需要分配已提交页面的物理存储或后备空间。
  • MEM_RESERVE表示保留进程虚拟地址空间的一段范围,而不分配任何实际的物理存储。
  • PAGE_READWRITE:此参数指定区域的内存保护。在这种情况下,它允许对分配的内存进行读取和写入。

 _RoundUp()对齐

inline size_t _RoundUp(size_t size, size_t alignsize)
{
	if (size % alignsize == 0)
	{
		return size / alignsize;
	}
	return size / alignsize + 1;
}

既然我们是选择以页为单位申请内存块, 这里我们在申请内存前就要将申请的内存大小向上取整对齐页的大小,这里的alignsize使用时就传入一页的字节大小。

New()

T* New()
{
	T* allocate = nullptr;
	if (_freelist)
	{
		allocate = (T*)_freelist;
		_freelist = *(void**)_freelist;
	}
	else 
	{
		if (_remainder < sizeof T)
		{
			size_t kpage = (_RoundUp(sizeof T * Nums, 1 << 13) >> 13);
			_remainder = sizeof T * Nums;
			_pool = (char*)malloc(_remainder);
			if (_pool == nullptr)
			{
				throw std::bad_alloc();
			}
		}
		allocate = (T*)_pool;
		size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
		_pool += objSize;
		_remainder -= objSize;
	}
	new(allocate) T(); //定位new,显示调用T的构造函数初始化
	if (allocate == nullptr)
	{
		int n = 0;
	}
	return allocate;
}
  • 用于从内存池中分配一个新的对象。如果有空闲对象可用,则从 _freelist 中获取一个,并将 _freelist 指向下一个空闲对象。
  • 如果没有空闲对象,则从内存池 _pool 中分配新的内存,如果剩余空间不足以容纳一个对象,则重新分配内存池。
  • 在分配的内存位置使用定位 new 构造一个类型为 T 的对象,并返回指向该对象的指针。

与malloc()做性能对比

完整代码:

#pragma once
#include <iostream>
#include <vector>
#include <time.h>
using std::cout;
using std::endl;
inline size_t _RoundUp(size_t size, size_t alignsize)
{
	if (size % alignsize == 0)
	{
		return size / alignsize;
	}
	return size / alignsize + 1;
}
template<class T, long long Nums>
class ObjMemoryPool
{
public:
	ObjMemoryPool()
		:_freelist(nullptr), _pool(nullptr), _remainder(0)
	{}
	T* New()
	{
		T* allocate = nullptr;
		if (_freelist)
		{
			allocate = (T*)_freelist;
			_freelist = *(void**)_freelist;
		}
		else 
		{
			if (_remainder < sizeof T)
			{
				size_t kpage = (_RoundUp(sizeof T * Nums, 1 << 13) >> 13);
				_remainder = sizeof T * Nums;
				_pool = (char*)malloc(_remainder);
				if (_pool == nullptr)
				{
					throw std::bad_alloc();
				}
			}
			allocate = (T*)_pool;
			size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
			_pool += objSize;
			_remainder -= objSize;
		}
		new(allocate) T(); //定位new,显示调用T的构造函数初始化
		if (allocate == nullptr)
		{
			int n = 0;
		}
		return allocate;
	}
	void Delete(T* obj)
	{
		obj->~T();
		*(void**)obj = _freelist;
		_freelist = obj;
	}
private:
	void* _freelist;
	char* _pool;
	size_t _remainder;
};
struct TreeNode
{
	int _val;
	TreeNode* _left;
	TreeNode* _right;

	TreeNode()
		:_val(0)
		, _left(nullptr)
		, _right(nullptr)
	{}
};
int main()
{
	// 申请释放的轮次
	const size_t Rounds = 5;
	// 每轮申请释放多少次
	const size_t N = 100000;
	std::vector<TreeNode*> v1;
	v1.reserve(N);
	size_t begin1 = clock();
	for (size_t j = 0; j < Rounds; ++j)
	{
		for (int i = 0; i < N; ++i)
		{
			v1.push_back(new TreeNode);
		}
		for (int i = 0; i < N; ++i)
		{
			delete v1[i];
		}
		v1.clear();
	}
	size_t end1 = clock();
	std::vector<TreeNode*> v2;
	v2.reserve(N);
	ObjMemoryPool<TreeNode,10000> TNPool;
	size_t begin2 = clock();
	for (size_t j = 0; j < Rounds; ++j)
	{
		for (int i = 0; i < N; ++i)
		{
			v2.push_back(TNPool.New());
		}
		for (int i = 0; i < N; ++i)
		{
			TNPool.Delete(v2[i]);
		}
		int x=1;
		v2.clear();
	}
	size_t end2 = clock();
	cout << "new cost time:" << end1 - begin1 << endl;
	cout << "object pool cost time:" << end2 - begin2 << endl;
	return 0;
}

1)定义了一个简单的二叉树节点结构 TreeNode,做为被测试申请释放的obj类型

2)在 main() 函数中:

  • 设置了两组实验参数:每轮申请释放的次数 N 和申请释放的轮次 Rounds。
  • 创建了两个空的 vector 容器 v1 和 v2,用于存放申请的节点指针。
  • 对第一组实验使用了直接使用 new 的方式进行内存分配和释放。通过循环在每轮中申请 N 个节点,然后释放它们,并清空容器。
  • 对第二组实验使用了自定义的对象内存池 ObjMemoryPool 进行内存分配和释放。同样地,通过循环在每轮中申请 N 个节点,然后释放它们,并清空容器。 记

3)录了每组实验的开始和结束时间,并输出两组实验的时间差,以比较它们的性能。 最后输出了两组实验的耗时情况。

测试结果:

这里我们可以明显看到,此时定长内存池的申请释放效率明显快过malloc和free

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/426683.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

思科网络设备监控

思科是 IT 行业的先驱之一&#xff0c;提供从交换机到刀片服务器的各种设备&#xff0c;以满足中小企业和企业的各种 IT 管理需求。管理充满思科的 IT 车间涉及许多管理挑战&#xff0c;例如监控可用性和性能、管理配置更改、存档防火墙日志、排除带宽问题等等&#xff0c;这需…

区块链媒体发布推广10个热门案例解析-华媒舍

区块链技术的发展已经引起了媒体的广泛关注&#xff0c;越来越多的区块链媒体纷纷发布推广相关的热门案例。本文将介绍10个成功的区块链媒体推广案例&#xff0c;并分享它们的成功秘诀&#xff0c;帮助读者更好地了解区块链媒体推广的方法与技巧。 随着区块链技术的成熟和应用场…

常用的电阻、电容的种类和应用场合?

电阻的 a.按阻值特性:固定电阻、可调电阻、特种电阻(敏感电阻)&#xff0c;不能调节的,我们称之为固定电阻,而可以调节的,我们称之为可调电阻.常见的例如收音机音量调节的,主要应用于电压分配的,我们称之为电位器. b.按制造材料:碳膜电阻、金属膜电阻、线绕电阻&#xff0c;捷…

Qt/C++音视频开发67-保存裸流加入sps/pps信息/支持264/265裸流/转码保存/拉流推流

一、前言 音视频组件除了支持保存MP4文件外&#xff0c;同时还支持保存裸流即264/265文件&#xff0c;以及解码后最原始的yuv文件。在实际使用过程中&#xff0c;会发现部分视频文件保存的裸流文件&#xff0c;并不能直接用播放器播放&#xff0c;查阅资料得知原来是缺少sps/p…

2023年,我的年终总结

序言 2023年的年终总结一直拖到现在&#xff0c;想来是有多个原因吧&#xff1a;第一个应该是年底还有些事情没有完成&#xff0c;内心有所不甘&#xff1b;第二个应该是这一年似乎是很忙碌的一年&#xff0c;不知从何说起&#xff1b;第三个应该是对于自己这一年的收获&#…

Learning from Unlabeled 3D Environments forVision-and-Language Navigation

这篇论文是关于高级指令的 摘要 在视觉和语言导航 (VLN) 中&#xff0c;实体代理需要按照自然语言指令在真实的 3D 环境中进行导航。现有 VLN 方法的一个主要瓶颈是缺乏足够的训练数据&#xff0c;导致对未见过的环境的泛化效果不理想。虽然 VLN 数据通常是手动收集的&#x…

动态SQL的处理

学习视频&#xff1a;3001 动态SQL中的元素_哔哩哔哩_bilibili 目录 1.1为什么学 1.2动态SQL中的元素 条件查询操作 if 元素 choose、when、otherwise元素 where、trim元素 更新操作 set元素使用场景 复杂查询操作 foreach 元素中的属性 ​编辑 迭代数组 迭代List 迭代Map 1…

RocketMQ - 深入研究一下Broker是如何持久化存储消息的

1. CommitLog消息顺序写入机制 首先思考一下,当生产者的消息发送到一个Broker上的时候,他接收到了一条消息,接着他会对这个消息做什么事情? 首先第一步,他会把整个消息直接写入磁盘上的一个日志文件,叫做CommitLog,直接顺序写入这个文件,如下图: 这个CommitLog是很…

Python用类实现抽象和封装

归纳编程学习的感悟&#xff0c; 记录奋斗路上的点滴&#xff0c; 希望能帮到一样刻苦的你&#xff01; 如有不足欢迎指正&#xff01; 共同学习交流&#xff01; &#x1f30e;欢迎各位→点赞 &#x1f44d; 收藏⭐ 留言​&#x1f4dd; 路在脚下&#xff0c;勇往直前&#x…

mac报错:zsh: command not found: npm

1、问题概述&#xff1f; 在mac系统中使用npm命令的时候&#xff0c;mac os报错提示&#xff1a; zsh: command not found: npm 一般出现发这种情况的原因时没有安装npm,而npm这命令时集成在nodejs中的&#xff0c;所以安装nodejs就可以了。 2、解决办法 本质就是需要安装…

Day13:信息打点-JS架构框架识别泄漏提取API接口枚举FUZZ爬虫插件项目

目录 JS前端架构-识别&分析 JS前端架构-开发框架分析 前端架构-半自动Burp分析 前端架构-自动化项目分析 思维导图 章节知识点 Web&#xff1a;语言/CMS/中间件/数据库/系统/WAF等 系统&#xff1a;操作系统/端口服务/网络环境/防火墙等 应用&#xff1a;APP对象/API接…

聊一聊Python量化交易

在金融领域&#xff0c;量化交易已经成为一种越来越受欢迎的交易方式。它通过使用数学模型来分析市场&#xff0c;自动化执行交易决策&#xff0c;以此来获取超额回报。近年来&#xff0c;Python因其简洁易学、功能强大而成为量化交易领域的首选编程语言。本文将详细介绍Python…

Vue 前端开发 v-for和v-if两个指令不能混合使用

原由&#xff1a; 在进行项目开发的时候因为在一个标签上同时使用了v-for和v-if两个指令导致的报错。 提示错误&#xff1a;The undefined variable inside v-for directive should be replaced with a computed property that returns filtered array instead. You should no…

NOC2023软件创意编程(学而思赛道)python初中组初赛真题

软件创意编程 一、参赛范围 1.参赛组别:小学低年级组(1-3 年级)、小学高年级组(4-6 年级)、初中组。 2.参赛人数:1 人。 3.指导教师:1 人(可空缺)。 4.每人限参加 1 个赛项。 组别确定:以地方教育行政主管部门(教委、教育厅、教育局) 认定的选手所属学段为准。 二、…

【新书推荐】11.1 子程序设计

第十一章 子程序及参数传递 本章先讲述子程序设计的方法&#xff0c;然后介绍在子程序调用中参数的四种传递方法。 11.1 子程序设计 在前面的示例和练习中&#xff0c;会发现程序中会有一些反复使用到的代码片段。我们将程序中反复出现的程序片段设计成子程序&#xff0c;这样…

蓝桥杯备赛 day1 | 1. 门牌制作, 2. 迷宫, 3. 乘积尾零

最近正好在刷算法题&#xff0c;报了一个蓝桥杯体验一下&#xff0c;但是钱都交了&#xff0c;高低混个奖好吧&#xff0c;今天做的都是一些填空推理题&#xff0c;相当于用程序写下正解&#xff0c;代码是在Dev C上面写的 #include<iostream> #include<bits/stdc.h&g…

ABAP - SALV教程13 使用自定义的状态栏

有时候SALV的默认标准按钮无法满足用户需求,需要在状态栏中添加自定义的按钮解决方法1&#xff1a;在默认的标准按钮上调用方法增加按钮&#xff1a;http://t.csdnimg.cn/rB7my解决方法2&#xff1a;使用自定义的状态栏&#xff0c;报错问题的解决&#xff1a;http://t.csdnimg…

Spring Boot整合Mybatis配置多数据源

Spring Boot 专栏&#xff1a;https://blog.csdn.net/dkbnull/category_9278145.html Spring Cloud 专栏&#xff1a;https://blog.csdn.net/dkbnull/category_9287932.html GitHub&#xff1a;https://github.com/dkbnull/SpringBootDemo Gitee&#xff1a;https://gitee.com/…

达梦运维工具-DEM搭建

运维监控工具-DEM 前言 根据达梦官网文档整理 一、工具介绍 DM企业管理器&#xff08;DM Enterprise Manager&#xff0c;简称为DEM&#xff09;提供一个通过Web 界面来监控、管理并维护DM数据库的集中式管理平台。数据库管理员可通过任意Web应用登录DEM&#xff0c;从而对…

rtt的io设备框架面向对象学习-io设备管理层

目录 1.设备基类2.rtt基类2.1 rtt基类定义2.2 对象容器定义2.3 rtt基类构造函数 3.io设备管理接口4.总结 这层我的理解就是rtt基类和设备基类所在&#xff0c;所以抽离出来好点&#xff0c;不然每个设备类都要重复它。 1.设备基类 /include/rtdef.h中定义了设备基类struct rt_…
最新文章