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

news2024/12/25 0:01:36

🌈 博客个人主页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.coloradmin.cn/o/1487144.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

P9905 [COCI 2023/2024 #1] AN2DL 【矩阵区间最大值】

文章目录 题目大意1.输入格式2.输出格式3.数据范围与约定 思路维护每一行区间维护每一列区间维护区间最大值code↓ 完结撒花(&#xffe3;▽&#xffe3;) / 题目大意 给定 n , m , r , s n,m,r,s n,m,r,s 和一个 n m n\times m nm 的整数矩阵 A A A&#xff0c;求它每个 …

PyTorch-神经网络

神经网络&#xff0c;这也是深度学习的基石&#xff0c;所谓的深度学习&#xff0c;也可以理解为很深层的神经网络。说起这里&#xff0c;有一个小段子&#xff0c;神经网络曾经被打入了冷宫&#xff0c;因为SVM派的崛起&#xff0c;SVM不了解的同学可以去google一下&#xff0…

Android 多桌面图标启动, 爬坑点击打开不同页面

备注 &#xff1a; MainActivity 正常带界面的UI MainActivityBt 和 MainActivityUsb 是透明的&#xff0c;即 android:theme"style/TranslucentTheme" ###场景1:只有MainActivity 设置成&#xff1a;android:launchMode"singleTask" 点击顺序&#xff1…

外贸网站模板建站

测绘检测wordpress外贸主题 简洁实用的wordpress外贸主题&#xff0c;适合做测绘检测仪器设备的外贸公司使用。 https://www.jianzhanpress.com/?p5337 白马非马衣服WordPress外贸建站模板 白马非马服装行业wordpress外贸建站模板&#xff0c;适用于时间服装企业的官方网站…

ehcache3介绍和使用示例

介绍 EhCache是一个广泛使用的Java进程内缓存框架&#xff0c;具有快速和精干的特点。它提供了以下主要优势&#xff1a; 速度快&#xff1a;由于其直接在JVM进程中运行&#xff0c;EhCache的访问速度非常快&#xff0c;适合对响应时间要求较高的应用。 配置灵活&#xff1a;…

使用 Haproxy 搭建Web群集

Haproxy是目前比较流行的一种群集调度工具&#xff0c;同类群集调度工具有很多&#xff0c;如LVS 和Nginx。相比较而言&#xff0c;LVS.牲能最好&#xff0e;但是搭建相对复杂:Nginx的upstream模块支持群集功能&#xff0e;但是对群集节点健康检查功能不强&#xff0c;性能没有…

GEE:使用ReLu激活函数对单波段图像进行变换(以NDVI为例)

作者:CSDN @ _养乐多_ 本文将介绍在 Google Earth Engine (GEE)平台上,对任意单波段影像进行 ReLu 变换的代码。并以对 NDVI 影像像素值的变换为例。 文章目录 一、ReLu激活函数1.1 什么是 ReLu 激活函数1.2 用到遥感图像上有什么用?二、代码链接三、完整代码一、ReLu激活…

HTML5:七天学会基础动画网页6

CSS3自定义字体 ①&#xff1a;首先需要下载所需字体 ②&#xff1a;把下载字体文件放入 font文件夹里&#xff0c;建议font文件夹与 css 和 image文件夹平级 ③&#xff1a;引入字体&#xff0c;可直接在html文件里用font-face引入字体&#xff0c;分别是字体名字和路径 例…

【C++ 函数栈】栈区保存函数参数和函数调用的过程

目录 1 调用过程 &#x1f64b;‍♂️ 作者&#xff1a;海码007&#x1f4dc; 专栏&#xff1a;C专栏&#x1f4a5; 标题&#xff1a;【C 函数栈】栈区保存函数参数和函数调用的过程❣️ 寄语&#xff1a;人生的意义或许可以发挥自己全部的潜力&#xff0c;所以加油吧&#xff…

循环队列与循环双端队列

文章目录 前言循环队列循环双端队列 前言 1、学习循环队列和循环双端队列能加深我们对队列的理解&#xff0c;提高我们的编程能力。 2、本文循环队列使用的是数组&#xff0c;循环双端队列用的是双向链表 3、题目连接&#xff1a;设计循环队列 &#xff0c;设计循环双端队列。 …

C++面试宝典第34题:整数反序

题目 给出一个不多于5位的整数, 进行反序处理。要求: 1、求出它是几位数。 2、分别输出每一位数字。仅数字间以空格间隔, 负号与数字之间不需要间隔。如果是负数,负号加在第一个数字之前, 与数字没有空格间隔。注意:最后一个数字后没有空格。 3、按逆序输出各位数字。逆序后…

华为od机试C卷-开源项目热度榜单

1、题目描述 某个开源社区希望将最近热度比较高的开源项目出一个榜单&#xff0c;推荐给社区里面的开发者。 对于每个开源项目&#xff0c;开发者可以进行关注(watch)、收藏(star)、fork、提issue、提交合并请求(MR)等。 数据库里面统计了每个开源项目关注、收藏、fork、issue…

适用于恢复iOS数据的 10 款免费 iPhone 恢复软件

现在&#xff0c;您可以获得的 iPhone 的存储容量比大多数人的笔记本电脑和台式电脑的存储容量还要大。虽然能够存储数千张高分辨率照片和视频文件、安装数百个应用程序并随身携带大量音乐库以供离线收听固然很棒&#xff0c;但在一个地方拥有如此多的数据可能会带来毁灭性的后…

LNOI省选祭录

写在前面 大概率爆零 你说得对&#xff0c;但是「辽宁省2024联合省选」是一款由「中国计算机学会」推出的一款「开放世界冒险游戏」&#xff0c;游戏发生在一个被称作「大连大学」的幻想世界&#xff0c;在这里&#xff0c;被神选中的人将被授予「xor魔法手杖」&#xff0c;引…

#WEB前端(浮动与定位)

1.实验&#xff1a; 2.IDE&#xff1a;VSCODE 3.记录&#xff1a; float、position 没有应用浮动前 应用左浮动和右浮动后 应用定位 4.代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><me…

matplotlib直方图

matplotlib直方图 假设你获取了250部电影的时长(列表a中), 希望统计出这些电影时长的分布状态(比如时长为100分钟到120分钟电影的数量, 出现的频率)等信息, 你应该如何呈现这些数据? from matplotlib import pyplot as plt a[131, 98, 125, 131, 124, 139, 131, 117, 128, …

linux逻辑卷管理

一.物理卷&#xff0c;逻辑卷&#xff0c;卷组的关系 二.实验题目 1.业务需要&#xff0c;新增5G硬盘&#xff0c;先对第一块磁盘分区&#xff0c;大小为4G&#xff0c;现在进行逻辑卷划分&#xff0c;卷组名为myvg,逻辑卷名为LV1&#xff0c;大小为2G 2.格式化逻辑卷LV1&#…

【论文阅读】多传感器SLAM数据集

一、M2DGR 该数据集主要针对的是地面机器人&#xff0c;文章正文提到&#xff0c;现在许多机器人在进行定位时&#xff0c;其视角以及移动速度与车或者无人机有着较大的差异&#xff0c;这一差异导致在地面机器人完成SLAM任务时并不能直接套用类似的数据集。针对这一问题该团队…

附加Numpy数组

参考&#xff1a;Append Numpy Array 引言 在数据科学和机器学习领域&#xff0c;处理大规模数据集是一项重要且常见的任务。为了高效地处理数据&#xff0c;numpy是一个非常强大的Python库。本文将详细介绍numpy中的一个重要操作&#xff0c;即如何附加&#xff08;append&a…

【网站项目】308学生档案管理系统

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…