定长内存池的实现、测试及错误分析

背景

C/C++ 申请内存使用的是 mallocmalloc 其实就是一个大众货,什么场景下都可以用,但是什么场景下都可以用就意味着什么场景下都不会有很高的性能。

定长内存池解决固定大小的内存申请释放需求, 性能达到极致,不考虑内存碎片等问题。

定长内存池成员设计

先分配一块大内存,为了方便对这段空间进行操作,将指针定义成 char 类型,想移动指针只需要加减 n 即可。

申请内存时,会判断大内存还有没有空间。如果大内存有空间,但不能满足需分配的小内存的需要,这时分配就会导致内存溢出。所以需要记录大内存剩余字节数 _remain_size

if(_memory == nullptr) {//malloc 分配定长内存
}
T* obj = (T*)_memory;
_memory += sizeof(T);

这时申请内存就不会出现上述问题了。

//剩余内存不够一个对象大小时,重新开内存
if (_remain_size < sizeof(T)) {_remain_size = 128 * 1024;_memory = (char*)malloc(_remain_size );if (_memory == nullptr) {throw std::bad_alloc(); }
}
obj = (T*)_memory;
_memory += objsize;
_remainSizeBytes -= objsize;

内存被申请了,如何还回来呢?

可以通过自由链表的方式管理内存块,但并不需要设计链表,可以用内存块的前 4 个字节(32位机器)或者前 8 个字节(64 位)作为一个指针指向下一个内存块,再用一个头指针 freelist 维护这个链表。

指针的类型决定了解引用后可访问的字节大小。在 32 位系统上,一个指针通常是 4 个字节;在 64 位系统上,一个指针通常是 8 个字节。

*(void**)obj = _freelist;
_freelist = obj;

obj 指针强制转换为 void** 类型,也就是指向指针的指针类型,然后对其解引用,实际上就是把 obj 所指向的内存位置当作一个指针变量来看待,并将当前自由链表的头指针 _freelist 的值赋给它,并更新自由链表的头指针。

所以,定长内存池需要三个成员

char* _memory = nullptr;  	// 指向大块内存的指针,char* 有利于拓展
size_t _remain_size=0;  	// 内存块后面剩余的字节大小
void* _free_list;       	// 自由链表管理还回来的内存

申请内存时也要优先使用自由链表中的内存。

//优先使用还回来的内存块对象
if (_free_list) {void* next = *((void**)_free_list);obj = _free_list;_free_list = next;
}

但是分配内存时还存在着问题:

size_t objsize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);

分配内存大小要满足上面代码, 方便还回来的时候连接到一起。

定长设计

利用非类型模板参数,让内存池每次申请的内存块大小都是固定的

template <size_t N>
class ObjectPool {};

或者使用模板实现,将内存池设计为模板类,创建一个内存池需要给它赋一个类型,这样每次申请的对象都是一个类型,做到了定长。

template <class T>
class ObjectPool {};

内存池如何申请对象

当内存池要申请对象时,优先使用前面释放的内存,即在 _free_list 中维护的内存块。

_free_list 为空,说明前面的空间没有释放,需要分配内存池后面的空间。

若后面的空间不够申请一个对象,内存池就重新向 OS 申请一块大内存,再申请对象。

不过这里定长内存池也是调用了 malloc 去申请大块的空间,能不能直接调用系统申请空间的接口呢?

可以直接向堆申请内存空间,在 Linux 下,可以调用 brkmmap 函数。在 Windows 下用的是 VirtualAlloc 函数。

brk 函数

brk 函数通常用于在堆的末尾扩展或收缩内存。

原型

#include <unistd.h>
void *brk(void *addr);

用法

  • addr 参数指定新的数据段末尾的地址。
  • 如果调用成功,返回新的数据段末尾的地址;如果失败,返回 (void *) -1 并设置 errno

示例

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>int main() {char *heap_start;// 获取当前堆的末尾heap_start = (char *) sbrk(0);// 请求增加1024字节的堆空间if (brk(heap_start + 1024) == (void *) -1) {perror("brk");exit(EXIT_FAILURE);}// 在新申请的堆空间中写入数据snprintf(heap_start, 1024, "Hello, heap!");printf("%s\n", heap_start);return 0;
}

优点

  • 相对简单,适用于小块内存的分配。

缺点

  • 不适合多线程环境,因为 brk 是全局的,会影响整个进程的堆。
  • 扩展堆空间时可能会导致内存碎片。
mmap 函数

mmap 函数用于在进程的地址空间中映射文件或匿名内存区域。它通常用于需要大块内存或希望避免内存碎片的场景。

原型

#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

用法

  • addr:建议映射的内存起始地址(通常传 NULL 让系统自动选择)。
  • length:映射区域的长度。
  • prot:期望的内存保护标志(如 PROT_READ, PROT_WRITE)。
  • flags:控制映射对象的类型和行为(如 MAP_PRIVATE, MAP_ANONYMOUS)。
  • fd:文件描述符(对于匿名内存映射,传 -1)。
  • offset:文件中的偏移量(对于匿名内存映射,传 0)。

示例

#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>int main() {size_t length = 1024;void *mapped_memory;// 匿名内存映射mapped_memory = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);if (mapped_memory == MAP_FAILED) {perror("mmap");exit(EXIT_FAILURE);}// 在映射的内存中写入数据snprintf(mapped_memory, length, "Hello, mmap!");printf("%s\n", (char *)mapped_memory);// 释放映射的内存if (munmap(mapped_memory, length) == -1) {perror("munmap");exit(EXIT_FAILURE);}return 0;
}

优点

  • 适用于大块内存分配。
  • 映射的内存区域可以独立管理,不容易受到其他内存分配的影响。
  • 可以更好地利用虚拟内存机制,提高内存管理的灵活性。

缺点

  • 相对复杂,需要更多的系统管理开销。
  • 需要手动释放内存(通过 munmap)。

选择使用 brk 还是 mmap 取决于具体的应用场景和需求。对于小块内存分配和简单的内存管理,brk 可能更合适;而对于大块内存分配和需要避免内存碎片的场景,mmap 通常更合适。

ObjectPool.h 的实现

#pragma once#include<iostream>#ifdef _WIN32#include<Windows.h> // Windows下的头文件
#else#include <sys/mman.h> // Linux下的头文件
#endif // _WIN32// 直接去堆上按页申请空间
// 每页大小 8KB(2^13),kpage 为页数,总大小为 kpage << 13 字节
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32 // Windows下的系统调用接口void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else// linux下brk mmap等void* ptr = mmap(0, kpage<<13, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
#endifif (ptr == nullptr)throw std::bad_alloc();return ptr;
}// 定长内存池模板类
template<class T>
class ObjectPool
{
public:// 申请一个T类型大小的空间T* New() {T* obj = nullptr;// 优先从空闲链表中分配if (_free_list) { // 取出空闲链表头部obj = (T*)_free_list;// 将 _free_list 更新为下一个空闲块(原头部存储的地址)_free_list = *(void**)_free_list;}// 当前内存块剩余空间不足,申请新的大块内存else {// _memory中剩余空间小于T的大小的时候再开空间if (_remain_size < sizeof(T)) {_remain_size = 128 * 1024; // 128KB(16页)//_memory = (char*)malloc(_remain_size);// 申请16页_memory = (char*)SystemAlloc(_remain_size >> 13); if (_memory == nullptr) {throw std::bad_alloc();}}// 计算步长:确保至少按指针大小划分,以兼容空闲链表指针操作size_t objsize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);obj = (T*)_memory;       // 分配当前内存块起始位置_memory += objsize;      // 移动内存块指针_remain_size -= objsize; // 更新剩余空间}// 定位 new 显式调用构造函数new(obj)T;return obj;}// 释放对象内存,将其加入空闲链表void Delete(T* obj) {// 显式调用析构函数obj->~T();// 将释放的内存头插到空闲链表*(void**)obj = _free_list; // 将 obj 的前 8 字节指向原链表头_free_list = obj;          // 更新链表头为当前释放的块}private:char* _memory = nullptr;     // 指向当前大块内存的起始地址size_t _remain_size = 0;     // 当前大块内存的剩余字节数void* _free_list = nullptr;  // 空闲链表头指针(维护返还的小内存块)
};

为什么 New() 中要用到定位 new ?

在最后返回申请到内存地址之前,使用了定位 new 的方法显示调用了 T 类型的默认构造函数去初始化这块对象空间。

new(obj)T;

这是有必要的,因为这块空间不是我们直接定义对象得到的,而是通过一个 T* 类型的指针变量 obj 指向一块内存得来的:

obj = (T*)_memory;

这种情况系统不会自动调用 T 类型的默认构造函数去初始化 obj 指向的这块内存空间,所以我们就在函数内部调用定位 new 来完成对这块内存空间初始化。

定位 new 的基本语法

定位 new 允许你在已经分配的内存中创建一个对象,而不再通过 new 来请求堆内存。它通常用于内存池管理、对象池等场景。

基本语法如下:

  • 调用默认构造函数(无初始化):

    new (place_address) type;
    

    这会在 place_address 指定的内存位置上调用 type 的默认构造函数,初始化对象。

  • 调用带参数的构造函数(使用初始化列表):

    new (place_address) type(initializer-list);
    

    这会在 place_address 指定的内存位置上调用 type 的带有初始化参数的构造函数。

定位 new 详解
  • place_address:这是你已经分配好的内存地址,通常是通过 mallocoperator new 或其他方法分配的内存地址。注意,place_address 必须是足够大的内存区域,足以存储你要创建的对象。

  • type:这是你要在 place_address 处构造的对象的类型。

  • initializer-list:这是一个用于初始化对象的列表,它会传递给 type 的构造函数。如果没有提供,默认构造函数会被调用。

默认构造函数
#include <iostream>class MyClass {
public:MyClass() {std::cout << "MyClass default constructor" << std::endl;}
};int main() {char buffer[sizeof(MyClass)]; // 预分配足够空间// 在 buffer 中创建 MyClass 对象,调用默认构造函数new (buffer) MyClass;return 0;
}
带参数的构造函数
#include <iostream>class MyClass {
public:MyClass(int a, int b) {std::cout << "MyClass constructor with arguments: " << a << ", " << b << std::endl;}
};int main() {char buffer[sizeof(MyClass)]; // 预分配足够空间// 在 buffer 中创建 MyClass 对象,调用带参数的构造函数new (buffer) MyClass(10, 20);return 0;
}
重要事项
  • 必须确保有足够的内存place_address 指定的内存区域必须足够大,能够容纳你要创建的对象。如果内存空间不足,可能会导致未定义行为(如内存越界)。

  • 显式调用析构函数:当你使用定位 new 创建对象时,需要手动显式调用对象的析构函数。如果你使用 new 分配内存并创建对象,C++ 会自动处理析构函数,但使用定位 new 后,你需要显式销毁对象。例如:

    obj->~MyClass();
    
  • 内存管理:定位 new 并不负责内存的管理。它只是告诉编译器在指定内存位置上构造对象,因此你需要自己负责内存的分配和回收。

在对象池中的应用示例

在对象池中,定位 new 可以帮助你在已分配的内存池中构建对象,而不是每次都调用 new 来分配新的内存空间。这样可以有效避免频繁的内存分配和释放。

template <typename T>
class ObjectPool {
public:T* allocate() {// 假设我们有一个已分配的足够大的内存区域char buffer[sizeof(T)];// 在预分配的内存中创建对象T* obj = new (buffer) T();return obj;}void deallocate(T* obj) {// 销毁对象,调用析构函数obj->~T();}
};

TestObjectPool.cpp 的实现

#include "ObjectPool.h"
#include <vector>// 树节点结构体定义
struct TreeNode {int _val;         // 节点存储的值TreeNode* _left;  // 左子节点指针TreeNode* _right; // 右子节点指针// 构造函数:初始化值为0,子节点指针为空TreeNode() : _val(0), _left(nullptr), _right(nullptr) {}
};// 内存池性能测试函数
void TestObjectPool() {// 测试参数配置const size_t Rounds = 10;   // 测试轮次(申请释放循环次数)const size_t N = 10000000;   // 每轮次操作次数/****************** 传统new/delete测试 ******************/size_t begin1 = clock();  // 记录测试开始时间std::vector<TreeNode*> v1; // 使用vector管理节点指针v1.reserve(N);             // 预分配内存避免扩容影响测试for (size_t j = 0; j < Rounds; ++j) {// 批量申请节点for (int i = 0; i < N; ++i) {v1.push_back(new TreeNode); // 每次new一个TreeNode}// 批量释放节点for (int i = 0; i < N; ++i) {delete v1[i]; // 逐个delete释放内存}v1.clear(); // 清空vector准备下一轮测试}size_t end1 = clock();  // 记录测试结束时间/****************** 内存池性能测试 ******************/ObjectPool<TreeNode> TNPool; // 创建TreeNode内存池实例size_t begin2 = clock();  // 记录内存池测试开始时间std::vector<TreeNode*> v2;v2.reserve(N);            // 预分配内存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(); // 清空vector}size_t end2 = clock();  // 记录内存池测试结束时间/****************** 结果输出 ******************/std::cout << "传统new/delete耗时: " << (end1 - begin1) / CLOCKS_PER_SEC << "s" << std::endl;std::cout << "定长内存池实现耗时:  " << (end2 - begin2) / CLOCKS_PER_SEC << "s" << std::endl;
}int main() {TestObjectPool();  // 执行性能测试return 0;
}

测试代码易错点

std::vector<TreeNode*> v2.reserve(N)std::vector<TreeNode*> v2(N) 的关键区别在于容器初始化方式内存行为,这是导致段错误的根本原因。

行为v2.reserve(N)v2(N)
初始化元素数量0N
内存分配策略预分配,避免扩容精确分配,无预留空间
指针安全性安全(地址有效)危险(扩容后地址失效)
适用场景已知最大元素数量的高效插入需要初始值的场景

结论:在需要动态插入元素且已知最大数量的场景中,始终优先使用 reserve() + push_back,避免混合使用初始化大小和动态插入。

推荐一下

https://github.com/0voice

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/70709.shtml

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

相关文章

vue3 下载文件 responseType-blob 或者 a标签

在 Vue 3 中&#xff0c;你可以使用 axios 或 fetch 来下载文件&#xff0c;并将 responseType 设置为 blob 以处理二进制数据。以下是一个使用 axios 的示例&#xff1a; 使用 axios 下载文件 首先&#xff0c;确保你已经安装了 axios&#xff1a; npm install axios然后在你…

Search API:让数据获取变得简单高效的搜索引擎代理商

Search API&#xff1a;让数据获取变得简单高效的搜索引擎代理商 在当今数字化时代&#xff0c;数据驱动的决策变得越来越重要&#xff0c;而获取精准、实时的数据是众多企业、研究机构和开发者的核心需求。然而&#xff0c;直接爬取搜索引擎或行业资讯网站可能会遇到诸多挑战&…

halcon三维对象处理例程总结(二)

目录 一、intersect_plane_object_model_3d二、interactive_intersection三、measure_plant四、moments_object_model_3d五、projective_trans_object_model_3d六、read_object_model_3d_generic_ascii一、intersect_plane_object_model_3d 计算三维物体模型与平面之间的相交部…

基于 Python 的项目管理系统开发

基于 Python 的项目管理系统开发 一、引言 在当今快节奏的工作环境中&#xff0c;有效的项目管理对于项目的成功至关重要。借助信息技术手段开发项目管理系统&#xff0c;能够显著提升项目管理的效率和质量。Python 作为一种功能强大、易于学习且具有丰富库支持的编程语言&…

2月24(信息差)

&#x1f30d;“任意舞蹈任意学”&#xff01;宇树机器人又进化了 传Meta有意合作抛出橄榄枝 &#x1f384;两部门&#xff1a;深入推进公路沿线充电基础设施建设 推动大功率充电技术标准应用 ✨小米15 Ultra、小米SU7 Ultra定档2月27日 雷军宣布&#xff1a;向超高端进发 1.…

mysql 迁移到人大金仓数据库

我是在windows上安装了客户端工具 运行数据库迁移工具 打开 在浏览器输入http://localhost:54523/ 账号密码都是kingbase 添加mysql源数据库连接 添加人大金仓目标数据库 添加好的两个数据库连接 新建迁移任务 选择数据库 全选 迁移中 如果整体迁移不过去可以单个单个或者几个…

C++和OpenGL实现3D游戏编程【连载23】——几何着色器和法线可视化

欢迎来到zhooyu的C++和OpenGL游戏专栏,专栏连载的所有精彩内容目录详见下边链接: 🔥C++和OpenGL实现3D游戏编程【总览】 1、本节实现的内容 上一节课,我们在Blend软件中导出经纬球模型时,遇到了经纬球法线导致我们在游戏中模型光照显示问题,我们在Blender软件中可以通过…

JUC并发—12.ThreadLocal源码分析

大纲 1.ThreadLocal的特点介绍 2.ThreadLocal的使用案例 3.ThreadLocal的内部结构 4.ThreadLocal的核心方法源码 5.ThreadLocalMap的核心方法源码 6.ThreadLocalMap的原理总结 1.ThreadLocal的特点介绍 (1)ThreadLocal的注释说明 (2)ThreadLocal的常用方法 (3)ThreadL…

Deepseek和Grok 3对比:写一段冒泡排序

1、这是访问Grok 3得到的结果 2、grok3输出的完整代码&#xff1a; def bubble_sort(arr):n len(arr) # 获取数组长度# 外层循环控制排序轮数for i in range(n):# 内层循环比较相邻元素&#xff0c;j的范围逐渐减少for j in range(0, n - i - 1):# 如果当前元素大于下一个元…

Java-01-源码篇-04集合-05-ConcurrentHashMap(1)

1.1 加载因子 加载因子&#xff08;Load Factor&#xff09;是用来决定什么时候需要扩容的一个参数。具体来说&#xff0c;加载因子 当前元素数量 / 桶的数量&#xff0c;当某个桶的元素个数超过了 桶的数量 加载因子 时&#xff0c;就会触发扩容。 我们都知道 ConcurrentHas…

vue3: directive自定义指令防止重复点击

第一章 前言 相信很多小伙伴会在各个渠道上搜如何防止重复点击&#xff0c;之后会推荐什么防抖、节流来避免这一操作&#xff0c;该方法小编就不继续往下说了。接下来说说小编的场景&#xff0c;项目已经完成的差不多了&#xff0c;但是由于之前大家都是直接点击事件调用方法的…

忽略Git文件的修改,让它不被提交

使用Git托管的工程中&#xff0c;经常有这样的需求&#xff0c;希望文件只是本地修改&#xff0c;不提交到服务端。 如果仅仅是本地存在的文件&#xff0c;我们可以通过.gitignore配置避免文件被提交。 有的时候文件是由git托管的&#xff0c;但是我们希望只在本地修改&#…

Zap:Go 的高性能日志库

文章目录 Zap&#xff1a;Go 高性能日志库一、Zap 的核心优势二、快速入门 Zap1. 安装2. 基本用法输出示例 三、Logger 与 SugaredLogger&#xff1a;如何选择&#xff1f;1. **Logger&#xff08;高性能模式&#xff09;**2. **SugaredLogger&#xff08;开发友好模式&#xf…

每日一题——顺时针旋转矩阵

顺时针旋转矩阵 目录 一、问题描述二、解题思路 1. 原地旋转矩阵2. 旋转逻辑3. 代码实现 三、代码解析 1. 参数说明2. 原地旋转逻辑3. 返回矩阵 四、示例测试代码五、复杂度分析 1. 时间复杂度2. 空间复杂度 一、问题描述 以下是内容转换为 CSDN 的 Markdown 格式&#xf…

接雨水的算法

题目 代码 # 接雨水算法 def trap(height):# 1. 特殊情况&#xff1a;数组为空 则返回0if not height:return 0n len(height)# 2. 初始化左右指针&#xff0c;左右最大值&#xff0c;结果left, right 0, n - 1# maxleft代表左边最大值&#xff0c;maxright代表右边最大值max…

会话对象 HttpSession 二、HttpSession失效

session失效有如下几个原因&#xff1a; session.invalidate()方法注销sessionsession超时 <session-config><!-- session的超时时间&#xff0c;以分钟为单位 --><session-timeout>1</session-timeout> </session-config>Cookie被禁用

Jenkins 创建 Node 到 Windows

Jenkins 创建 Node 到 Windows 一. 新建 Node Dashboard -> Manage Jenkins -> Manage Nodes and Clouds Dashboard -> Nodes -> New Node 二. 配置节点 Node&#xff1a;节点名 Description&#xff1a;节点描述 Number of executors&#xff1a;节点最大同…

Opengl常用缓冲对象功能介绍及使用示例(C++实现)

本文整理了常用的opengl缓冲区对象并安排了使用示例 名称英文全称作用简述顶点数组对象Vertex Array Object (VAO)管理 VBO 和 EBO 的配置&#xff0c;存储顶点属性设置&#xff0c;简化渲染流程&#xff0c;避免重复设置状态顶点缓冲区对象Vertex Buffer Object (VBO)存储顶点…

矩阵加减乘除的意义与应用

矩阵加法 数学意义 线性空间的封闭性线性变换的叠加矩阵分解与表示 实际应用 数据聚合与统计图像处理与计算机视觉物理学与工程学动态系统与优化经济学与运筹学信号处理与通信游戏开发与计算机图形学环境科学与地理信息矩阵加法的关键特点 矩阵减法 数学意义线性空间封闭性 线…

【Redis原理】底层数据结构 五种数据类型

文章目录 动态字符串SDS(simple dynamic string )SDS结构定义SDS动态扩容 IntSetIntSet 结构定义IntSet的升级 DictDict结构定义Dict的扩容Dict的收缩Dict 的rehash ZipListZipListEntryencoding 编码字符串整数 ZipList的连锁更新问题 QuickListQuickList源码 SkipListRedisOb…