【项目设计】高并发内存池(一)[项目介绍|内存池介绍|定长内存池的实现]
🎇C++学习历程:入门
- 博客主页:一起去看日落吗
- 持续分享博主的C++学习历程
博主的能力有限,出现错误希望大家不吝赐教
- 分享给大家一句我很喜欢的话: 也许你现在做的事情,暂时看不到成果,但不要忘记,树🌿成长之前也要扎根,也要在漫长的时光🌞中沉淀养分。静下来想一想,哪有这么多的天赋异禀,那些让你羡慕的优秀的人也都曾默默地翻山越岭🐾。
💐 🌸 🌷 🍀
目录
- 💐1. 项目介绍
- 💐2. 内存池介绍
- 💐3. 定长内存池的实现
- 💐4. 定长内存池代码
- 💐5. 性能测试
💐1. 项目介绍
-
本项目实现的是一个高并发的内存池,它的原型是Google的一个开源项目tcmalloc,tcmalloc全称Thread-Caching Malloc,即线程缓存的malloc,实现了高效的多线程内存管理,用于替换系统的内存分配相关函数malloc和free。
-
tcmalloc的知名度也是非常高的,不少公司都在用它,比如Go语言就直接用它做了自己的内存分配器。
-
该项目就是把tcmalloc中最核心的框架简化后拿出来,模拟实现出一个mini版的高并发内存池,目的就是学习tcmalloc的精华。
-
该项目主要涉及C/C++、数据结构(链表、哈希桶)、操作系统内存管理、单例模式、多线程、互斥锁等方面的技术。
💐2. 内存池介绍
- 池化技术
在说内存池之前,我们得先了解一下“池化技术”。所谓“池化技术”,就是程序先向系统申请过量的资源,然后自己进行管理,以备不时之需。
之所以要申请过量的资源,是因为申请和释放资源都有较大的开销,不如提前申请一些资源放入“池”中,当需要资源时直接从“池”中获取,不需要时就将该资源重新放回“池”中即可。这样使用时就会变得非常快捷,可以大大提高程序的运行效率。
在计算机中,有很多使用“池”这种技术的地方,除了内存池之外,还有连接池、线程池、对象池等。以服务器上的线程池为例,它的主要思想就是:先启动若干数量的线程,让它们处于睡眠状态,当接收到客户端的请求时,唤醒池中某个睡眠的线程,让它来处理客户端的请求,当处理完这个请求后,线程又进入睡眠状态。
- 内存池
内存池是指程序预先向操作系统申请一块足够大的内存,此后,当程序中需要申请内存的时候,不是直接向操作系统申请,而是直接从内存池中获取;同理,当释放内存的时候,并不是真正将内存返回给操作系统,而是将内存返回给内存池。当程序退出时(或某个特定时间),内存池才将之前申请的内存真正释放。
- 内存池主要解决的问题
内存池主要解决的就是效率的问题,它能够避免让程序频繁的向系统申请和释放内存。其次,内存池作为系统的内存分配器,还需要尝试解决内存碎片的问题。
内存碎片分为内部碎片和外部碎片:
- 外部碎片是一些空闲的小块内存区域,由于这些内存空间不连续,以至于合计的内存足够,但是不能满足一些内存分配申请需求。
- 内部碎片是由于一些对齐的需求,导致分配出去的空间中一些内存无法被利用。
注意: 内存池尝试解决的是外部碎片的问题,同时也尽可能的减少内部碎片的产生。
- malloc
C/C++中我们要动态申请内存并不是直接去堆申请的,而是通过malloc函数去申请的,包括C++中的new实际上也是封装了malloc函数的。
我们申请内存块时是先调用malloc,malloc再去向操作系统申请内存。malloc实际就是一个内存池,malloc相当于向操作系统“批发”了一块较大的内存空间,然后“零售”给程序用,当全部“售完”或程序有大量的内存需求时,再根据实际需求向操作系统“进货”。
malloc的实现方式有很多种,一般不同编译器平台用的都是不同的。比如Windows的VS系列中的malloc就是微软自行实现的,而Linux下的gcc用的是glibc中的ptmalloc。
💐3. 定长内存池的实现
malloc其实就是一个通用的内存池,在什么场景下都可以使用,但这也意味着malloc在什么场景下都不会有很高的性能,因为malloc并不是针对某种场景专门设计的。
定长内存池就是针对固定大小内存块的申请和释放的内存池,由于定长内存池只需要支持固定大小内存块的申请和释放,因此我们可以将其性能做到极致,并且在实现定长内存池时不需要考虑内存碎片等问题,因为我们申请/释放的都是固定大小的内存块。
我们可以通过实现定长内存池来熟悉一下对简单内存池的控制,其次,这个定长内存池后面会作为高并发内存池的一个基础组件。
- 如何实现定长?
在实现定长内存池时要做到“定长”有很多种方法,比如我们可以使用非类型模板参数,使得在该内存池中申请到的对象的大小都是N。
template<size_t N>
class ObjectPool
{};
此外,定长内存池也叫做对象池,在创建对象池时,对象池可以根据传入的对象类型的大小来实现“定长”,因此我们可以通过使用模板参数来实现“定长”,比如创建定长内存池时传入的对象类型是int,那么该内存池就只支持4字节大小内存的申请和释放。
template<class T>
class ObjectPool
{};
- 如何直接向堆申请空间?
既然是内存池,那么我们首先得向系统申请一块内存空间,然后对其进行管理。要想直接向堆申请内存空间,在Windows下,可以调用VirtualAlloc函数;在Linux下,可以调用brk或mmap函数。
#ifdef _WIN32#include <Windows.h>
#else//...
#endif//直接去堆上申请按页申请空间
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32void* ptr = VirtualAlloc(0, kpage<<13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else// linux下brk mmap等
#endifif (ptr == nullptr)throw std::bad_alloc();return ptr;
}
这里我们可以通过条件编译将对应平台下向堆申请内存的函数进行封装,此后我们就不必再关心当前所在平台,当我们需要直接向堆申请内存时直接调用我们封装后的SystemAlloc函数即
- 定长内存池中应该包含哪些成员变量?
对于向堆申请到的大块内存,我们可以用一个指针来对其进行管理,但仅用一个指针肯定是不够的,我们还需要用一个变量来记录这块内存的长度。
由于此后我们需要将这块内存进行切分,为了方便切分操作,指向这块内存的指针最好是字符指针,因为指针的类型决定了指针向前或向后走一步有多大距离,对于字符指针来说,当我们需要向后移动n个字节时,直接对字符指针进行加n操作即可。
其次,释放回来的定长内存块也需要被管理,我们可以将这些释放回来的定长内存块链接成一个链表,这里我们将管理释放回来的内存块的链表叫做自由链表,为了能找到这个自由链表,我们还需要一个指向自由链表的指针。
因此,定长内存池当中包含三个成员变量:
-
_memory:指向大块内存的指针。
-
_remainBytes:大块内存切分过程中剩余字节数。
-
_freeList:还回来过程中链接的自由链表的头指针。
-
内存池如何管理释放的对象?
对于还回来的定长内存块,我们可以用自由链表将其链接起来,但我们并不需要为其专门定义链式结构,我们可以让内存块的前4个字节(32位平台)或8个字节(64位平台)作为指针,存储后面内存块的起始地址即可。
因此在向自由链表插入被释放的内存块时,先让该内存块的前4个字节或8个字节存储自由链表中第一个内存块的地址,然后再让_freeList指向该内存块即可,也就是一个简单的链表头插操作。
如何让一个指针在32位平台下解引用后能向后访问4个字节,在64位平台下解引用后能向后访问8个字节?
首先我们得知道,32位平台下指针的大小是4个字节,64位平台下指针的大小是8个字节。而指针指向数据的类型,决定了指针解引用后能向后访问的空间大小,因此我们这里需要的是一个指向指针的指针,这里使用二级指针就行了。
当我们需要访问一个内存块的前4/8个字节时,我们就可以先该内存块的地址先强转为二级指针,由于二级指针存储的是一级指针的地址,二级指针解引用能向后访问一个指针的大小,因此在32位平台下访问的就是4个字节,在64位平台下访问的就是8个字节,此时我们访问到了该内存块的前4/8个字节。
void*& NextObj(void* ptr)
{return (*(void**)ptr);
}
需要注意的是,在释放对象时,我们应该显示调用该对象的析构函数清理该对象,因为该对象可能还管理着其他某些资源,如果不对其进行清理那么这些资源将无法被释放,就会导致内存泄漏。
//释放对象
void Delete(T* obj)
{//显示调用T的析构函数清理对象obj->~T();//将释放的对象头插到自由链表NextObj(obj) = _freeList;_freeList = obj;
}
- 内存池如何为我们申请对象?
当我们申请对象时,内存池应该优先把还回来的内存块对象再次重复利用,因此如果自由链表当中有内存块的话,就直接从自由链表头删一个内存块进行返回即可。
如果自由链表当中没有内存块,那么我们就在大块内存中切出定长的内存块进行返回,当内存块切出后及时更新_memory指针的指向,以及_remainBytes的值即可。
需要特别注意的是,由于当内存块释放时我们需要将内存块链接到自由链表当中,因此我们必须保证切出来的对象至少能够存储得下一个地址,所以当对象的大小小于当前所在平台指针的大小时,需要按指针的大小进行内存块的切分。
此外,当大块内存已经不足以切分出一个对象时,我们就应该调用我们封装的SystemAlloc函数,再次向堆申请一块内存空间,此时也要注意及时更新_memory指针的指向,以及_remainBytes的值。
//申请对象
T* New()
{T* obj = nullptr;//优先把还回来的内存块对象,再次重复利用if (_freeList != nullptr){//从自由链表头删一个对象obj = (T*)_freeList;_freeList = NextObj(_freeList);}else{//保证对象能够存储得下地址size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);//剩余内存不够一个对象大小时,则重新开大块空间if (_remainBytes < objSize){_remainBytes = 128 * 1024;_memory = (char*)SystemAlloc(_remainBytes >> 13);if (_memory == nullptr){throw std::bad_alloc();}}//从大块内存中切出objSize字节的内存obj = (T*)_memory;_memory += objSize;_remainBytes -= objSize;}//定位new,显示调用T的构造函数初始化new(obj)T;return obj;
}
需要注意的是,与释放对象时需要显示调用该对象的析构函数一样,当内存块切分出来后,我们也应该使用定位new,显示调用该对象的构造函数对其进行初始化。
💐4. 定长内存池代码
//
// ObjectPool.hpp
// ObjectPool
//
// Created by 卜绎皓 on 2023/1/28.
//#include <iostream>
#include <vector>
#include <time.h>
using std::cout;
using std::endl;//定长内存池
template<class T>
class ObjectPool
{
public://申请对象T* New(){T* obj = nullptr;//优先把还回来的内存对象重复利用if(_freeList != nullptr){//从自由链表删除一个对象void* next = *((void**)_freeList);obj = (T*)_freeList;_freeList = next;}else{//保证对象能够储存下地址size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);//剩余内存不够一个对象大小时,则重新开大空间if(_remainBytes < objSize){_remainBytes = 128 * 1024;_memory = (char*)malloc(_remainBytes);if(_memory == nullptr)throw std::bad_alloc();}//从大块内存中切除objSize字节的内容obj = (T*)_memory;_memory += objSize;_remainBytes -= objSize;}//定位new,显示调用T的构造函数初始化new(obj) T;return obj;}//释放对象void Delete(T* obj){//显示调用T的析构函数清理对象obj->~T();//将释放的对象头插到自由链表*(void**)obj = _freeList;_freeList = obj;}private:char* _memory = nullptr; // 指向大块内存的指针size_t _remainBytes = 0; // 大块内存切分剩余的字节数void* _freeList = nullptr; // 还回来过程中链接的自由链表的头指针
};
💐5. 性能测试
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 = 1000000;std::vector<TreeNode*> v1;v1.reserve(N);//malloc和freesize_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();//定长内存池ObjectPool<TreeNode> TNPool;std::vector<TreeNode*> v2;v2.reserve(N);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]);}v2.clear();}size_t end2 = clock();cout << "new cost time:" << end1 - begin1 << endl;cout << "object pool cost time:" << end2 - begin2 << endl;
}
在代码中,我们先用new申请若干个TreeNode对象,然后再用delete将这些对象再释放,通过clock函数得到整个过程消耗的时间。(new和delete底层就是封装的malloc和free)
然后再重复该过程,只不过将其中的new和delete替换为定长内存池当中的New和Delete,此时再通过clock函数得到该过程消耗的时间。
可以看到在这个过程中,定长内存池消耗的时间比malloc/free消耗的时间要短。这就是因为malloc是一个通用的内存池,而定长内存池是专门针对申请定长对象而设计的,因此在这种特殊场景下定长内存池的效率更高,正所谓“尺有所短,寸有所长”。
相关文章:

【项目设计】高并发内存池(一)[项目介绍|内存池介绍|定长内存池的实现]
🎇C学习历程:入门 博客主页:一起去看日落吗持续分享博主的C学习历程博主的能力有限,出现错误希望大家不吝赐教分享给大家一句我很喜欢的话: 也许你现在做的事情,暂时看不到成果,但不要忘记&…...

初识MySQL下载与安装【快速掌握知识点】
目录 前言 MySQL版本 MySQL类型 MySQL官网有.zip和.msi两种安装形式; MySQL 下载 1、MySQL 属于 Oracle 旗下产品,进入Oracle官网下载 2、点击产品,找到MySQL 3、进入MySQL页面 4、点击Download(下载)&#x…...

如何终止一个线程
如何终止一个线程 是使用 thread.stop() 吗? public class ThreadDemo extends Thread{Overridepublic void run() {try {Thread.sleep(10000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("this is demo thread :"Thre…...

上岸!选择你的隐私计算导师!
开放隐私计算 开放隐私计算开放隐私计算OpenMPC是国内第一个且影响力最大的隐私计算开放社区。社区秉承开放共享的精神,专注于隐私计算行业的研究与布道。社区致力于隐私计算技术的传播,愿成为中国 “隐私计算最后一公里的服务区”。183篇原创内容公众号…...

go gin学习记录5
有了前面几节的学习,如果做个简单的web服务端已经可以完成了。 这节来做一下优化。 我们实验了3种SQL写入的方法,但是发现每一种都需要在方法中去做数据库链接的操作,有些重复了。 所以,我们把这部分提取出来,数据库链…...

PyQt5数据库开发2 5.1 QSqlQueryModel
目录 一、Qt窗体设计 1. 新建Qt项目 2. 拷贝4-3的部分组件过来 3. 添加资源文件 4. 创建Action 5. 添加工具栏 6. 创建菜单项 7. 关闭Action的实现 8. 调整布局 8.1 调整两个groupbox的布局 8.3 为窗体设置全局布局 二、代码拷贝和删除 1. 新建项目目录 2. 编译…...

MySQL-redo log和undo log
什么是事务 事务是由数据库中一系列的访问和更新组成的逻辑执行单元 事务的逻辑单元中可以是一条SQL语句,也可以是一段SQL逻辑,这段逻辑要么全部执行成功,要么全部执行失败 举个最常见的例子,你早上出去买早餐,支付…...

阿里云ECS TOP性能提升超20%!KeenTune助力倚天+Alinux3达成开机即用的全栈性能调优 | 龙蜥技术
文/KeenTune SIG01阿里云 ECS 上售卖页新增“应用加速”功能2023年1月12日 阿里云 ECS 的售卖页有了一些新的变化,在用户选择倚天 Alinux3 新建实例时,多了一个新的选项“应用加速”。这个功能是 阿里云 ECS 基于 KeenTune 提供典型云场景的开机即用的全…...

华为OD机试真题Python实现【快递业务站】真题+解题思路+代码(20222023)
快递业务站 题目 快递业务范围有 N 个站点,A 站点与 B 站点可以中转快递,则认为 A-B 站可达, 如果 A-B 可达,B-C 可达,则 A-C 可达。 现在给 N 个站点编号 0、1、…n-1,用 s[i][j]表示 i-j 是否可达, s[i][j] = 1表示 i-j可达,s[i][j] = 0表示 i-j 不可达。 现用二维…...

【c语言】预处理
🚀write in front🚀 📜所属专栏:> c语言学习 🛰️博客主页:睿睿的博客主页 🛰️代码仓库:🎉VS2022_C语言仓库 🎡您的点赞、关注、收藏、评论,是…...

嵌入式常用知识
12、并发和并行的区别? 最本质的区别就是:并发是轮流处理多个任务,并行是同时处理多个任务。 你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。 你吃饭吃到一半&…...

和平精英五曜赐福返场,老款玛莎返场来了
和平精英五曜赐福返场,老款玛莎返场来了!新款如何选择! 关于返场的新消息,都说云南百收SEO解说消息不准,之前看过文章的应该会知道,全网只有云南百收SEO解说发了。玛莎返场,快喊你的阿姨来看&a…...

React从入门到精通二
React从入门到精通之购物车案例1. 购物车需求说明使用到的data list2. 项目code1. 购物车需求说明 list data展示到列表中每个item的通过按钮来控制购买的数据量删除按钮可以删除当前的itemTotal Price计算当前购物车的总的价格 使用到的data list const books [{id: 1,name…...

【likeshop多商户】电子面单商家直播上线啦~
likeshop多商户商城v2.2.0版本更新啦! 新增功能: 商家直播 单子面单 优化: 个人中心优惠券数量统计优化 修复: 秒杀商品待审核时,下单价格计算错误 个人中心修改头像后地址保存错误 「商家直播」 提升品牌知名度…...

游戏化销售管理是什么?使用CRM系统进行有什么用?
对于企业销售来说,高薪酬也伴随着更高的压力与挑战。高强度的单一工作会让销售人员逐渐失去对工作的兴趣,导致销售状态缺少动力和激情,工作开展愈加困难。您可以通过CRM系统进行游戏化销售管理,让销售人员重新干劲满满。 游戏并不…...

Mysql 索引(三)—— 不同索引的创建方式(主键索引、普通索引、唯一键索引)
了解了主键索引的底层原理,主键索引其实就是根据主键字段建立相关的数据结构(B树),此后在使用主键字段作为条件查询时,会直接根据主键查找B树的叶子结点。除了主键索引外,普通索引和唯一键索引也是如此&…...

秒懂算法 | 基于朴素贝叶斯算法的垃圾信息的识别
本文将带领大家亲手实现一个垃圾信息过滤的算法。 在正式讲解算法之前,最重要的是对整个任务有一个全面的认识,包括算法的输入和输出、可能会用到的技术,以及技术大致的流程。 本任务的目标是去识别一条短信是否为垃圾信息,即输入为一条文本信息,输出为二分类的分类结果。…...

SpringCloud - Feign远程调用
目录 Feign的远程调用 RestTemplate方式调用存在的问题 介绍与初步使用 Feign的自定义配置 Feign运行自定义配置来覆盖默认配置,可以修改的配置如下: 配置Feign日志有两种方式: Feign性能优化 Feign底层的客户端实现: 连…...

Eotalk Vol.03:结合 API DaaS,让使用数据更方便
Eotalk 是由 Eolink CEO 刘昊臻发起的泛技术聊天活动,每期都会邀请一些技术圈内的大牛聊聊天,聊些关于技术、创业工作、投融资等热点话题。 Eotalk 的第 3 期,很高兴邀请到 Tapdata CEO TJ 唐建法,TJ 可以说是一位超级大咖&#x…...

从零开始学习Java编程:一份详细指南
Java入门Java简介和历史Java开发环境的安装和配置Java开发工具的介绍和使用(例如Eclipse、IntelliJ IDEA等)Java语言的基本概念(例如变量、数据类型、运算符、流程控制语句等)面向对象编程基础面向对象编程概念和基本原则类和对象…...

电子技术——系统性分析反馈电压放大器
电子技术——系统性分析反馈电压放大器 在本节我们提供一个系统性的分析反馈电压放大器的方法。首先我们考虑反馈网络没有负载效应理想情况,其次我们考虑反馈网络有限阻抗下的非理想情况。总之,这种方法的思路在于,将非理想情况转换为理想情况…...

【C语言进阶】结构体、位段、枚举、以及联合(共用体)的相关原理与使用
📝个人主页:Sherry的成长之路 🏠学习社区:Sherry的成长之路(个人社区) 📖专栏链接:C语言进阶 🎯长路漫漫浩浩,万事皆有期待 文章目录1.结构体1.1 概述&a…...

《蓝桥杯每日一题》哈希·AcWing 2058. 笨拙的手指
1.题目描述每当贝茜将数字转换为一个新的进制并写下结果时,她总是将其中的某一位数字写错。例如,如果她将数字 14 转换为二进制数,那么正确的结果应为 1110,但她可能会写下 0110 或 1111。贝茜不会额外添加或删除数字,…...

Linux 定时任务调度(crontab)
一、Crontab Crontab命令用于设置周期性被执行的指令。该命令从标准输入设备读取指令,并将其存放于“crontab”文件中,以供之后读取和执行。 可以使用Crontab定时处理离线任务,比如每天凌晨2点更新数据等,经常用于系统任务调度。…...

C进阶:6.C语言文件操作
目录 1.为什么使用文件 2.什么是文件 2.1程序文件 2.2数据文件 2.3文件名 3.文件的打开和关闭 3.1文件指针 4.文件的顺序读写 fputc()写入文件 fgetc()从文件中读取 fgets()读取一段字符串 fprintf格式化写入文件、fscanf格式化读出文件 4.1对比一组函数 5.文件…...

Linux环境变量
Linux环境变量孤儿进程进程优先级其他概念环境变量感性的理解环境变量常见的环境变量添加环境变量环境变量的组织形式通过代码如何获取环境变量再次理解环境变量命令行参数孤儿进程 概念:父进程先于子进程结束,这样的子进程就叫做“孤儿进程”; “孤儿”…...

Kotlin-委托、代理和单例对象
委托和代理 实现委托和代理,使用的是by关键字。 这里设计一个场景:假设某个演员被要求唱歌,但是不会唱歌,就委托一个会唱歌的歌手在后台唱歌。 如何实现这个需求,下面就开始直接写代码 首先定义一个唱歌能力接口 int…...

华为OD机试真题Python实现【报数】真题+解题思路+代码(20222023)
报数 题目 一百个人围成一圈,每个人有一个编码编号从一开始到一百。 他们从一开始依次报数,报道M的人自动退出圈圈, 然后下一个人接着从1开始报数一直到剩余人数小于M。 请问最后剩余人在原先的编码为多少? 🔥🔥🔥🔥🔥👉👉👉👉👉👉 华为OD机试(Py…...

MacOS:Error message “error:0308010C:digital envelope routines::unsupported“
命令行:export NODE_OPTIONS--openssl-legacy-provider 原帖:https://stackoverflow.com/questions/69692842/error-message-error0308010cdigital-envelope-routinesunsupported...

Java 异常处理,超详细整理,适合新手入门
目录 前言 抛出异常 捕获异常 处理异常 finally块 总结 前言 当Java程序中出现错误或异常时,通常会抛出一个异常。Java的异常处理机制使得我们可以在程序运行过程中捕获这些异常并采取相应的措施,以便程序能够正常运行或者优雅地停止。 抛出异常 在…...