项目介绍 + 定长内存池设计及实现

| |
文章目录
- 项目介绍
- 当前项目做的是什么?
- 技术栈
- 内存池是什么?
- 池化技术
- 内存池
- 内存池主要解决的问题
- 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 亿次展…...
IDEA运行Tomcat出现乱码问题解决汇总
最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…...
MVC 数据库
MVC 数据库 引言 在软件开发领域,Model-View-Controller(MVC)是一种流行的软件架构模式,它将应用程序分为三个核心组件:模型(Model)、视图(View)和控制器(Controller)。这种模式有助于提高代码的可维护性和可扩展性。本文将深入探讨MVC架构与数据库之间的关系,以…...
leetcodeSQL解题:3564. 季节性销售分析
leetcodeSQL解题:3564. 季节性销售分析 题目: 表:sales ---------------------- | Column Name | Type | ---------------------- | sale_id | int | | product_id | int | | sale_date | date | | quantity | int | | price | decimal | -…...
第 86 场周赛:矩阵中的幻方、钥匙和房间、将数组拆分成斐波那契序列、猜猜这个单词
Q1、[中等] 矩阵中的幻方 1、题目描述 3 x 3 的幻方是一个填充有 从 1 到 9 的不同数字的 3 x 3 矩阵,其中每行,每列以及两条对角线上的各数之和都相等。 给定一个由整数组成的row x col 的 grid,其中有多少个 3 3 的 “幻方” 子矩阵&am…...
Spring数据访问模块设计
前面我们已经完成了IoC和web模块的设计,聪明的码友立马就知道了,该到数据访问模块了,要不就这俩玩个6啊,查库势在必行,至此,它来了。 一、核心设计理念 1、痛点在哪 应用离不开数据(数据库、No…...
【Java学习笔记】BigInteger 和 BigDecimal 类
BigInteger 和 BigDecimal 类 二者共有的常见方法 方法功能add加subtract减multiply乘divide除 注意点:传参类型必须是类对象 一、BigInteger 1. 作用:适合保存比较大的整型数 2. 使用说明 创建BigInteger对象 传入字符串 3. 代码示例 import j…...
《C++ 模板》
目录 函数模板 类模板 非类型模板参数 模板特化 函数模板特化 类模板的特化 模板,就像一个模具,里面可以将不同类型的材料做成一个形状,其分为函数模板和类模板。 函数模板 函数模板可以简化函数重载的代码。格式:templa…...
腾讯云V3签名
想要接入腾讯云的Api,必然先按其文档计算出所要求的签名。 之前也调用过腾讯云的接口,但总是卡在签名这一步,最后放弃选择SDK,这次终于自己代码实现。 可能腾讯云翻新了接口文档,现在阅读起来,清晰了很多&…...
Caliper 负载(Workload)详细解析
Caliper 负载(Workload)详细解析 负载(Workload)是 Caliper 性能测试的核心部分,它定义了测试期间要执行的具体合约调用行为和交易模式。下面我将全面深入地讲解负载的各个方面。 一、负载模块基本结构 一个典型的负载模块(如 workload.js)包含以下基本结构: use strict;/…...
渗透实战PortSwigger靶场:lab13存储型DOM XSS详解
进来是需要留言的,先用做简单的 html 标签测试 发现面的</h1>不见了 数据包中找到了一个loadCommentsWithVulnerableEscapeHtml.js 他是把用户输入的<>进行 html 编码,输入的<>当成字符串处理回显到页面中,看来只是把用户输…...
