中国公路建设行业协会网站上,餐厅网站建设什么科目,成都赢星高考培训学校,wordpress调用模版#x1f308; 博客个人主页#xff1a;Chris在Coding #x1f3a5; 本文所属专栏#xff1a;[高并发内存池] ❤️ 前置学习专栏#xff1a;[Linux学习] ⏰ 我们仍在旅途 目录 前言 项目介绍
1.内存池 1.1 什么是内存池 池化技术 内存池 1.2 为什… 博客个人主页Chris在Coding 本文所属专栏[高并发内存池] ❤️ 前置学习专栏[Linux学习] ⏰ 我们仍在旅途 目录 前言 项目介绍
1.内存池 1.1 什么是内存池 池化技术 内存池 1.2 为什么需要内存池 效率原因 内存碎片问题 1.3 实现定长内存池理解池化技术 定长内存池的设计 _freelist的设计 New()和Delete()的设计 与malloc()做性能对比 前言 项目介绍
首先,本项目的原型是Google的一个开源项目TCMallocTCMalloc是谷歌开发的一种内存分配器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那样复杂的寻找合适大小的空间。这种做法能够显著减少内存分配和释放的开销消除内部碎片。 通过从零实现一个定长内存池,我们可以更直接的理解池化技术,同时也是在为高并发内存池项目的实现做铺垫。 templateclass 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
#endifinline static void* SystemAlloc(size_t kpage) {
#ifdef _WIN32void* ptr VirtualAlloc(0, kpage 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#endifif (ptr nullptr)throw std::bad_alloc();return ptr;
}
inline static void SystemFree(void* ptr)
{
#ifdef _WIN32VirtualFree(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;
}
templateclass 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::vectorTreeNode* 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::vectorTreeNode* v2;v2.reserve(N);ObjMemoryPoolTreeNode,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 x1;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