【高并发内存池——项目】定长内存池——开胃小菜

发布于:2025-09-16 ⋅ 阅读:(34) ⋅ 点赞:(0)

提示:高并发内存池完整项目代码,在主页专栏项目中

先设计一个定长的内存池

       作为程序员(C/C++)我们知道申请内存使⽤的是malloc,malloc其实就是⼀个通⽤的⼤众货,什么场景 下都可以⽤,但是什么场景下都可以⽤就意味着什么场景下都不会有很⾼的性能,下⾯我们就先来设 计⼀个定⻓内存池做个开胃菜,当然这个定⻓内存池在我们后⾯的⾼并发内存池中也是有价值的,所 以学习他⽬的有两层,先熟悉⼀下简单内存池是如何控制的,第⼆他会作为我们后⾯内存池的⼀个基础组件。

#include <iostream>
#include <vector>
#include <time.h>
using std::cout;
using std::endl;

#ifdef _WIN32
	#include<windows.h>
#else
// 
#endif

// 定长内存池
//template<size_t N>
//class ObjectPool
//{};

// 直接去堆上按页申请空间
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
	void* ptr = VirtualAlloc(0, kpage<<13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
	// linux下brk mmap等
#endif

	if (ptr == nullptr)
		throw std::bad_alloc();

	return ptr;
}

template<class T>
class ObjectPool
{
public:
	T* New()
	{
		T* obj = nullptr;

		// 优先把还回来内存块对象,再次重复利用
		if (_freeList)
		{
			void* next = *((void**)_freeList);
			obj = (T*)_freeList;
			_freeList = next;
		}
		else
		{
			// 剩余内存不够一个对象大小时,则重新开大块空间
			if (_remainBytes < sizeof(T))
			{
				_remainBytes = 128 * 1024;
				//_memory = (char*)malloc(_remainBytes);
				_memory = (char*)SystemAlloc(_remainBytes >> 13);
				if (_memory == nullptr)
				{
					throw std::bad_alloc();
				}
			}

			obj = (T*)_memory;
			size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
			_memory += objSize;
			_remainBytes -= objSize;
		}

		// 定位new,显示调用T的构造函数初始化
		new(obj)T;

		return obj;
	}

	void Delete(T* obj)
	{
		// 显示调用析构函数清理对象
		obj->~T();

		// 头插
		*(void**)obj = _freeList;
		_freeList = obj;
	}

private:
	char* _memory = nullptr; // 指向大块内存的指针
	size_t _remainBytes = 0; // 大块内存在切分过程中剩余字节数

	void* _freeList = nullptr; // 还回来过程中链接的自由链表的头指针
};

一、为什么需要定长内存池?

在C++开发中,频繁的内存分配和释放是性能瓶颈的主要来源之一。让我们先看一个现实中的比喻:

🏢 传统内存分配的痛点

想象每次需要办公桌时都现买:

  • ⏰ 时间开销大:每次都要去家具市场

  • 💰 成本高昂:中间商赚差价(内存碎片)

  • 🎯 效率低下:无法批量优化

🏭 内存池的解决方案

像大型办公室统一采购:

  • 🚀 批量获取:一次性申请大块内存

  • ⚡ 快速分配:直接从池中分配,无需系统调用

  • 🔄 重复利用:释放的内存放回池中复用

  • 📦 减少碎片:固定大小分配,无外部碎片

二、定长内存池核心设计思想

1. 整体架构

三大核心组件:

  • 🗃️ 大块内存:从系统申请的内存块

  • 🔗 空闲链表:管理已释放可重用的内存块

  • 📊 分配策略:决定如何分配新内存

2. 类定义解析

template<class T>
class ObjectPool
{
private:
    char* _memory = nullptr;     // 指向大块内存的指针
    size_t _remainBytes = 0;     // 剩余可用字节数
    void* _freeList = nullptr;   // 空闲链表头指针
};

三、关键技术实现深度解析

1. 内存申请策略

// 直接去堆上按页申请空间
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
    void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
    // Linux下使用brk或mmap等系统调用
#endif

    if (ptr == nullptr)
        throw std::bad_alloc();

    return ptr;
}

设计要点:

  • 🖥️ 跨平台支持:Windows使用VirtualAlloc,Linux使用mmap

  • 📏 按页分配:以页面为单位(通常4KB),减少系统调用次数

  • 🚨 异常安全:分配失败抛出bad_alloc异常

2. 内存分配(New方法)

T* New()
{
    T* obj = nullptr;

    // 优先复用空闲链表中的内存块
    if (_freeList)
    {
        void* next = *((void**)_freeList);  // 获取下一个空闲块
        obj = (T*)_freeList;                // 当前块作为分配对象
        _freeList = next;                   // 更新空闲链表头
    }
    else
    {
        // 剩余内存不足时申请新的大块内存
        if (_remainBytes < sizeof(T))
        {
            _remainBytes = 128 * 1024;  // 申请128KB
            _memory = (char*)SystemAlloc(_remainBytes >> 13); // 计算页数
            if (_memory == nullptr)
            {
                throw std::bad_alloc();
            }
        }

        // 从大块内存中切分
        size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
        obj = (T*)_memory;
        _memory += objSize;
        _remainBytes -= objSize;
    }

    // 定位new调用构造函数
    new(obj)T;
    return obj;
}

关键技术点:

  1. 空闲链表优先:先尝试从空闲链表获取已释放的内存

  2. 内存对齐:确保每个对象至少sizeof(void*)大小,便于链表操作

  3. 批量申请:一次性申请128KB内存,减少系统调用

  4. 定位new:在指定内存地址调用构造函数

3. 内存释放(Delete方法)

void Delete(T* obj)
{
    // 调用析构函数清理对象
    obj->~T();

    // 头插法将内存块加入空闲链表
    *(void**)obj = _freeList;
    _freeList = obj;
}

设计精髓:

  • 🧹 资源清理:显式调用析构函数

  • 🔗 链表管理:使用头插法将释放的内存加入空闲链表

  • ⚡ 高效复用:释放的内存立即可用于下次分配

四、空闲链表技术的巧妙运用

1. 链表存储原理

关键技巧: 利用内存块本身存储链表指针

 

// 释放时:将当前内存块的前4/8字节存储下一个节点的地址
*(void**)obj = _freeList;  // 将_freeList值存入obj指向的内存
_freeList = obj;           // 更新链表头

// 分配时:从链表头取出节点,并更新头指针
void* next = *((void**)_freeList);  // 读取下一个节点地址
obj = (T*)_freeList;                // 当前节点作为分配对象
_freeList = next;                   // 更新链表头

2. 内存对齐的重要性

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

为什么需要对齐?

  • 📏 最小大小:确保每个内存块至少能存储一个指针(4或8字节)

  • 🧩 地址对齐:保证指针操作的正确性

  • ⚡ 访问效率:对齐的内存访问速度更快

五、性能优势分析

与传统malloc对比

特性 传统malloc 定长内存池
分配速度 较慢(系统调用) 极快(直接操作内存)
内存碎片 可能产生外部碎片 无外部碎片
系统调用 每次分配都可能调用 批量申请,极少调用
线程安全 需要加锁 可设计为线程本地
适用场景 通用分配 固定大小对象