项目介绍 + 定长内存池设计及实现
|
文章目录
- 项目介绍
- 当前项目做的是什么?
- 技术栈
- 内存池是什么?
- 池化技术
- 内存池
- 内存池主要解决的问题
- malloc
- 定长内存池
- 学习目的
- 定长内存池设计
项目介绍
当前项目做的是什么?
这个项目是实现一个高并发的内存池
, 它的原型是 Google 的一个开源项目 TCMalloc, 全称是 Thread-Caching Malloc, 即线程缓存的 malloc, 实现了高效的多线程内存管理
, 用于替代系统中的内存分配函数(malloc, free).
这个项目并不是把 TCMalloc 从头到尾实现一遍, 而是把 TCMalloc 中最核心的框架简化后拿出来, 模拟实现一个我们自己的 mini 版本的高并发内存池, 目的就是学习 TCMalloc 的精华, 有点类似我们之前学习 STL 容器的方式(模拟实现, 不是为了造轮子, 而是向当时顶尖的C++前辈学习, 同时也方便我们更好的理解这部分内容).
技术栈
这个项目的技术栈主要用到了 C/C++, 数据结构(链表, 哈希桶), 单例模式, 操作系统内存管理, 互斥锁, 多线程等方面的知识.
内存池是什么?
池化技术
所谓的"池化技术", 指的是程序先向系统申请过量的资源, 然后自己管理, 以备不时之需.
之所以要申请过量的资源, 是因为每次申请该资源都有较大的开销, 所以我们提前申请好了, 这样使用时就会变得非常快捷, 以便提高程序的运行效率
.
在计算机中, 有很多使用"池"这种技术的地方, 除了内存池, 常见的还有连接池, 线程池, 对象池等. 我们之前学习过线程池, 所以这里就以服务器上的线程池为例, 它的主要思想是: 一开始先启动若干数量的线程, 让它们处于睡眠状态, 当接受到客户端的请求时, 唤醒池中某个睡眠的线程, 让它来处理客户端的请求, 当处理完这个请求, 线程又进入睡眠状态.
内存池
内存池是指程序预先从操作系统中申请一块足够大的内存, 在此之后, 当程序中需要申请内存的时候, 不再是直接向操作系统申请, 而是直接从内存池中获取; 同理, 当程序释放内存的时候, 并不是真正将内存返回给操作系统, 而是返回给内存池. 当程序退出或在特定的时间, 内存池才将之前申请的内存真正释放
.
内存池主要解决的问题
内存池主要解决的当然是效率的问题, 这是毋庸置疑的, 其次如果站在系统的内存分配器的角度, 还需要解决一下内存碎片的问题. 说到这里, 那什么是内存碎片呢?
string 对象和 list 对象销毁后, 释放空间, 所以图中还有256Bytes的空间, 但是此时我们要申请超过128Bytes的空间却申请不出来, 因为这两块空间不连续了, 即所谓的碎片化.
这里还需要补充说明的是: 内存碎片实际上分为外碎片和内碎片, 上面我们讲的是外碎片问题.
- 外碎片是一些空闲的小块内存区域,由于这些内存空间不连续,以至于合计的内存足够,但是不能满足一些内存分配申请需求。
- 内碎片是由于一些对齐的需要,导致分配出去的空间中一些内存无法被利用。
malloc
对于 malloc 函数, 我们是不陌生的, 因为在 C/C++ 中我们要动态申请内存都是通过调用 malloc 函数去申请(C++中的 new, 底层也是封装了 malloc 函数), 但是我们要知道, 实际上我们不是直接去堆上获取内存的, 我们所熟知的 malloc, 本质就是一个内存池
.
用一个形象的比喻就是, malloc 函数相当于向操作系统"批发"了一块较大的内存空间, 然后"零售"给程序用. 当全部"售完"或程序有大量的内存需求时, 再根据实际需求向操作系统"进货".
malloc 的实现方式有很多种, 一般不同的编译器平台用的都是不同的. 比如常见的Windows的VS系列的编译器用的是微软自己写的一套, Linux gcc 用的是 glibc 中的 ptmalloc.
定长内存池
学习目的
作为 C/C++ 程序员, 我们知道申请内存使用的是 malloc, malloc 其实就是一个通用的大众货, 什么场景下都可以用, 这也就意味着它在什么场景下都不会有很高的性能, 而我们所学习的 tcmalloc 在多线程环境下比 malloc 性能高得多
.
之所以先实现一个定长内存池, 是因为它在我们后面的高并发内存池中也是有价值的, 所以学习定长内存池有两个目的:
- 熟悉简单内存池是如何控制的;
- 将作为高并发内存池的一个基础组件.
定长内存池设计
所谓的定长内存池, 顾名思义就是固定大小的内存.
在讲解之前呢, 我们先解决一个问题:
如何直接向堆申请内存?
因为是内存池, 所以我们首先得向系统申请一块内存空间,然后对其进行管理。如果想直接向堆申请内存空间,在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;
}
如何实现定长内存池中的定长?
一开始我们可以使用非类型的模板参数, 使得在该内存池中申请到的对象的大小都是N。
template<size_t N>
class ObjectPool
{};
但是考虑到定长内存池在后面会作为高并发内存池的一个基础组件, 所以这里我们使用模板参数来实现定长. 比如创建定长内存池时传入的对象类型是int,那么该定长内存池就只支持4字节大小内存的申请和释放。
template<class T>
class ObjectPool
{};
如何设计定长内存池?
我们直接向堆中申请大块内存后, 需要使用一个指针对其进行管理, 由于需要对大块内存进行切分, 所以仅一个指针是不够的, 还需要一个整型变量用于标识内存块中剩余字节数.
同时为了便于对大块内存的切分操作, 该指针类型使用char*, 而不是void*, 因为指针类型决定了执行±操作向前或向后走一步的步长, 而 void* 的解引用和±操作都是没有意义的.
补充内容:指针类型的意义
- 指针类型决定了指针在解引用的时候一次能访问几个字节(也叫指针的权限)
- 指针类型决定了指针向前或向后走一步的步长(±整数),单位是字节.
释放回来的小块内存也是需要被管理的, 那如何管理呢?可以使用一个链表对其进行管理, 定义一个指针指向这个链表的头, 我们把这个管理释放回来的小块内存的链表叫做自由链表.
对于释放回来的小块内存, 不需要专门为它们定义一个链式结构, 我们可以让小块内存的前4个字节(32位平台)或前8个字节(64位平台)存储后一个小块内存的首地址.
综上所述, 定长内存池的成员变量有:
- _memory: 指向大块内存的指针
- _leftBytes: 大块内存剩余的字节数
- _freeList: 管理还回来的内存对象的自由链表
内存池如何申请对象?
内存池申请对象的时候需要注意, 可以从大块内存中取, 也可以从自由链表中取, 如果自由链表有内存块对象的时候, 优先从自由链表中取, 即头删, 时间复杂度是O(1).
如果自由链表中没有内存块对象的时候, 那么我们就要在大块内存中切出定长的内存对象, 需要注意切出后及时更新 _memory 的指向和 _leftBytes 的值.
当大块内存剩余的字节数不足以存储下一个地址的值时, 则需要调用上方的 SystemAlloc 函数重新开辟大块空间.
代码实现:
// 申请空间
T* New()
{T* obj = nullptr;// 如果自由链表有对象, 直接取出一个(头删)// 优先从自由链表中取内存if (_freeList){obj = (T*)_freeList;_freeList = *((void**)_freeList);}else{// 注意:不管内存块对象有对大, 至少要存的下一个地址的值size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);// 剩余内存不够开辟一个对象大小时, 则重新开辟大块内存if (_leftBytes < objSize){_leftBytes = 128 * 1024;// _memory = (char*)malloc(_leftBytes);_memory = (char*)SystemAlloc(_leftBytes >> 13);if (_memory == nullptr){throw std::bad_alloc();}}obj = (T*)_memory;_memory += objSize;_leftBytes -= objSize;}// 使用定位new调用T的构造函数初始化// 对已分配的原始内存空间中显示调用构造函数初始化new(obj)T;return obj;
}
内存池如何管理释放的对象?
将释放回来的内存块对象头插进自由链表, 时间复杂度是O(1)
试想我们如何保证一个指针解引用后在32位平台下能够访问4个字节, 在64位平台下能够访问8个字节? 前面我们说了, 指针类型决定了指针在解引用操作时一次能够访问几个字节.
所以只要是二级指针
都能够完成上述要求.
我们将其封装成为一个函数:
void*& NextObj(void* ptr)
{return (*(void**)ptr);
}
还有一点需要注意,在释放对象的时候,我们应该显示调用该对象的析构函数清理该对象,因为该对象可能还管理着其他某些资源,如果不对其进行清理那么这些资源将无法被释放,从而导致内存泄漏。
代码实现:
// 释放对象
void Delete(T* obj)
{// 显示调用T的析构函数进行清理obj->~T();// 头插到_freeListNextObj(obj) = _freeList;_freeList = obj;
}
定长内存池整体代码
//定长内存池
template<class T>
class ObjectPool
{
public:T* New(){T* obj = nullptr;// 如果自由链表有对象, 直接取出一个(头删)// 优先从自由链表中取内存if (_freeList){obj = (T*)_freeList;_freeList = NextObj(_freeList);}else{// 注意:不管T对象有对大, 至少要存的下一个地址的值(4/8)size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);// 剩余内存不够开辟一个对象大小时, 则重新开辟大块内存if (_leftBytes < objSize){_leftBytes = 128 * 1024;// _memory = (char*)malloc(_leftBytes);_memory = (char*)SystemAlloc(_leftBytes >> 13);if (_memory == nullptr){throw std::bad_alloc();}}obj = (T*)_memory;_memory += objSize;_leftBytes -= objSize;}// 使用定位new调用T的构造函数初始化// 对已分配的原始内存空间中显示调用构造函数初始化new(obj)T;return obj;}void*& NextObj(void* ptr){return (*(void**)ptr);}void Delete(T* obj){// 显示调用T的析构函数进行清理obj->~T();// 头插到_freeListNextObj(obj) = _freeList;_freeList = obj;}private:char* _memory = nullptr; // 指向大块内存的指针size_t _leftBytes = 0; // 大块内存剩余的字节数void* _freeList = nullptr; // 管理还回来的内存对象的自由链表(头指针)
};
性能测试
对比测试 malloc, free 与定长内存池:
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 和 Delete 明显比 malloc 和 free 消耗的时间少, 这是因为在申请定长的内存时, 定长内存池比 malloc 要高效, 毕竟定长内存池是为了申请定长的内存对象而专门设计的.
相关文章:

项目介绍 + 定长内存池设计及实现
你好,我是安然无虞。 文章目录项目介绍当前项目做的是什么?技术栈内存池是什么?池化技术内存池内存池主要解决的问题malloc定长内存池学习目的定长内存池设计项目介绍 当前项目做的是什么? 这个项目是实现一个高并发的内存池, 它的原型是 Google 的一个开源项…...
Linux--线程安全的单例模式--自旋锁--0211
1. 线程安全的单例模式 1.1 什么是单例模式 某些类, 只应该具有一个对象(实例), 就称之为单例. 1.1.1 懒汉方式实现单例模式 以上篇博文的线程池为例 Liunx--线程池的实现--0208 09_Gosolo!的博客-CSDN博客 实现懒汉模式首先要先将构造函数私有化,…...

图文解说S参数(进阶篇)
S参数是RF工程师/SI工程师必须掌握的内容,业界已有多位大师写过关于S参数的文章,即便如此,在相关领域打滚多年的人, 可能还是会被一些问题困扰着。你懂S参数吗? 图文解说S参数(基础篇) 请继续往下看...台湾…...

Sentinel源码阅读
基础介绍 Sentinel 的使用可以分为两个部分: 核心库(Java 客户端):不依赖任何框架/库,能够运行于 Java 8 及以上的版本的运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持(见 主流框架适配&…...

2023年浙江食品安全管理员考试真题题库及答案
百分百题库提供食品安全管理员考试试题、食品安全管理员考试预测题、食品安全管理员考试真题、食品安全管理员证考试题库等,提供在线做题刷题,在线模拟考试,助你考试轻松过关。 一、判断题 7.(重点)《餐饮服务食品安全…...

Webstorm 代码没有提示,uniapp 标签报错
问题 项目是用脚手架创建的: vue create -p dcloudio/uni-preset-vue my-project 打开之后,添加view标签警告报错的。代码也没有提示,按官方说法:CLI 工程默认带了 uni-app 语法提示和 5App 语法提示。 但是我这里就是有问题。…...

MySQL-Innodb引擎事务原理
文章目录1.事务介绍2 事务特性3. 事务的实现原理4 redo log 保证持久性5 undo log 保证原子性6 MVCC 概念6.1 隐藏字段6.2 版本链6.3 ReadView6.3.1readview 版本控制规则7 隔离性 实现7.2 隔离性- REPEATABLE READ 可重复读下8 一致性1.事务介绍 事务是一组操作的集合…...

Linux操作系统学习(了解环境变量)
文章目录环境变量初识除了上述介绍的PATH,还有一些常见的环境变量如:查看环境变量方法 :环境变量的基本概念:本地变量:环境变量初识 环境变量解释起来比较抽象,先看示例: #include <stdio.…...

数据分析思维(六)|循环/闭环思维
循环/闭环思维 1、概念 在很多的分析场景下,我们需要按照一套流程反复分析,而不是进行一次性的分析,也就是说这套流程的结果会成为该流程的新一次输入,从而形成一个闭环,此时的分析思维我们称之为循环/闭环思维。 常…...

C++:类和对象(下)
文章目录1 再谈构造函数1.1 构造函数体赋值1.2 初始化列表1.3 explicit关键字2 static成员2.1 概念2.2 特性3 友元3.1 友元函数(流插入(<<)及流提取(>>)运算符重载)3.2 友元类4 内部类5 匿名对…...
ASP.NET Core MVC 项目 AOP之IResultFilter和IAsyncResultFilter
目录 一:说明 二:IActionFilter同步 三:IAsyncActionFilter异步 一:说明 IResultFilter同步过滤器与IAsyncResultFilter异步过滤器常常被用作于渲染视图或处理结果。 IResultFilter同步过滤器执行顺序: 1:执行控制器中的构造函数,实例化控制器 2:执行具体的Acti…...

jstack排查cpu占用高[复习]
这样就可以看到占用CPU高的代码位置。 总结:就是先查到占用高的应用和具体的线程,然后根据线程到堆积信息查找即可。 不过堆栈信息非十进制,需提前把线程号转为十六进制。 这样就可以看到占用CPU高的代码位置。 总结:就是先查到…...

网络安全-Pyhton环境搭建
网络安全-Pyhton环境搭建 https://www.kali.org/get-kali/#kali-installer-images—kali官网下载地址 python这个东东呢 是目前来说最简单,方便的开源的脚本语言 广泛用于Web开发,AI,网站开发等领域 python要装2和3 为什么要安装两个版本…...

SpringBoot Mybatis 分页实战
pageInfo的属性 pageNum:当前页 pageSize:页面数据量 startRow:当前页首条数据为总数据的第几条 endRow:当前页最后一条数据为总数据的第几条 total:总数据量 pages:总页面数 listPage{}结果集 reasonable …...

计算机断层扫描结肠镜和全自动骨密度仪在一次检查中的可行性
计算机断层扫描结肠镜和全自动骨密度仪在一次检查中的可行性 Feasibility of Simultaneous Computed Tomographic Colonography and Fully Automated Bone Mineral Densitometry in a Single Examination 简单总结: 数据:患者的结肠镜检查和腹部CT检查…...

Java多级缓存是为了解决什么的?
前言 提到缓存,想必每一位软件工程师都不陌生,它是目前架构设计中提高性能最直接的方式。 缓存技术存在于应用场景的方方面面。从网站提高性能的角度分析,缓存可以放在浏览器,可以放在反向代理服务器,还可以放…...

MongoDB--》索引的了解及具体操作
目录 索引—index 索引的类型 索引的管理操作 索引的使用 索引—index 使用索引的原因:索引支持在MongoDB中高效地执行查询。如果没有索引,MongoDB必须执行全集合扫描,即扫描集合中的每个文档,以选择与查询语句匹配的文档。这…...

Python open()函数详解:打开指定文件
在 Python 中,如果想要操作文件,首先需要创建或者打开指定的文件,并创建一个文件对象,而这些工作可以通过内置的 open() 函数实现。open() 函数用于创建或打开指定文件,该函数的常用语法格式如下:file ope…...

CentOS Stream 9尝鲜安装教程
作者:IT圈黎俊杰 一、下载CentOS Stream 9安装介质 在CentOS官网可以下载到CentOS Stream 9的安装介质,正面列出ISO介质的下载链接地址: https://download.cf.centos.org/9-stream/BaseOS/x86_64/iso/CentOS-Stream-9-20221019.0-x86_64-dv…...

Ambire AdEx 2023 年路线图
Ambire AdEx 是为简化 web3 显示广告而建立的,领先于时代。到 2023 年,它将专注于服务用户需求,同时保持其作为区块链隐私解决方案的核心,反对传统的数字广告模式。 回顾 2022 年 2022 年,AdEx 网络处理了超过 1 亿次展…...

【WiFi帧结构】
文章目录 帧结构MAC头部管理帧 帧结构 Wi-Fi的帧分为三部分组成:MAC头部frame bodyFCS,其中MAC是固定格式的,frame body是可变长度。 MAC头部有frame control,duration,address1,address2,addre…...

Redis相关知识总结(缓存雪崩,缓存穿透,缓存击穿,Redis实现分布式锁,如何保持数据库和缓存一致)
文章目录 1.什么是Redis?2.为什么要使用redis作为mysql的缓存?3.什么是缓存雪崩、缓存穿透、缓存击穿?3.1缓存雪崩3.1.1 大量缓存同时过期3.1.2 Redis宕机 3.2 缓存击穿3.3 缓存穿透3.4 总结 4. 数据库和缓存如何保持一致性5. Redis实现分布式…...

安宝特方案丨XRSOP人员作业标准化管理平台:AR智慧点检验收套件
在选煤厂、化工厂、钢铁厂等过程生产型企业,其生产设备的运行效率和非计划停机对工业制造效益有较大影响。 随着企业自动化和智能化建设的推进,需提前预防假检、错检、漏检,推动智慧生产运维系统数据的流动和现场赋能应用。同时,…...

《从零掌握MIPI CSI-2: 协议精解与FPGA摄像头开发实战》-- CSI-2 协议详细解析 (一)
CSI-2 协议详细解析 (一) 1. CSI-2层定义(CSI-2 Layer Definitions) 分层结构 :CSI-2协议分为6层: 物理层(PHY Layer) : 定义电气特性、时钟机制和传输介质(导线&#…...

Opencv中的addweighted函数
一.addweighted函数作用 addweighted()是OpenCV库中用于图像处理的函数,主要功能是将两个输入图像(尺寸和类型相同)按照指定的权重进行加权叠加(图像融合),并添加一个标量值&#x…...
质量体系的重要
质量体系是为确保产品、服务或过程质量满足规定要求,由相互关联的要素构成的有机整体。其核心内容可归纳为以下五个方面: 🏛️ 一、组织架构与职责 质量体系明确组织内各部门、岗位的职责与权限,形成层级清晰的管理网络…...

Keil 中设置 STM32 Flash 和 RAM 地址详解
文章目录 Keil 中设置 STM32 Flash 和 RAM 地址详解一、Flash 和 RAM 配置界面(Target 选项卡)1. IROM1(用于配置 Flash)2. IRAM1(用于配置 RAM)二、链接器设置界面(Linker 选项卡)1. 勾选“Use Memory Layout from Target Dialog”2. 查看链接器参数(如果没有勾选上面…...

苍穹外卖--缓存菜品
1.问题说明 用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大 2.实现思路 通过Redis来缓存菜品数据,减少数据库查询操作。 缓存逻辑分析: ①每个分类下的菜品保持一份缓存数据…...
解决本地部署 SmolVLM2 大语言模型运行 flash-attn 报错
出现的问题 安装 flash-attn 会一直卡在 build 那一步或者运行报错 解决办法 是因为你安装的 flash-attn 版本没有对应上,所以报错,到 https://github.com/Dao-AILab/flash-attention/releases 下载对应版本,cu、torch、cp 的版本一定要对…...

WordPress插件:AI多语言写作与智能配图、免费AI模型、SEO文章生成
厌倦手动写WordPress文章?AI自动生成,效率提升10倍! 支持多语言、自动配图、定时发布,让内容创作更轻松! AI内容生成 → 不想每天写文章?AI一键生成高质量内容!多语言支持 → 跨境电商必备&am…...