高并发内存池

https://blog.csdn.net/chenlong_cxy/article/details/122819562# 如何实现定长

非模板类型

当使用非类型模板参数定义内存池时,内存池中申请的对象大小会固定为 N。这种设计确保了内存池中每个对象的大小都一致,从而简化了内存管理。
固定大小:
使用非类型模板参数时,可以在模板中指定一个大小 N,这意味着所有通过该内存池分配的对象都将具有相同的大小 N。
类型无关:
这种方法与具体的对象类型无关,它仅关注对象的大小。因此,您可以创建一个内存池,专门用于分配固定大小的内存块,而不考虑对象的实际类型。

template<size_t N> 是 C++ 中使用非类型模板参数的一个示例。在这里,N 是一个编译时常量,用于表示模板的大小。具体来说,这种模板设计允许我们创建定长内存池或其他数据结构,其对象的大小在编译时就确定

#include <iostream>
#include <vector>
#include <cstdlib>

template<size_t N>  // N 是对象大小
class ObjectPool {
public:
    ObjectPool(size_t poolSize) {
        pool.reserve(poolSize);
        for (size_t i = 0; i < poolSize; ++i) {
            void* obj = ::operator new(N); // 分配 N 字节的内存
            pool.push_back(obj); // 将指针存入池中
        }
    }

    ~ObjectPool() {
        for (auto obj : pool) {
            ::operator delete(obj); // 释放内存
        }
    }

    void* allocate() {
        if (pool.empty()) return nullptr; // 如果池为空,返回空指针
        void* obj = pool.back(); // 获取池中的最后一个对象
        pool.pop_back(); // 从池中移除该对象
        return obj; // 返回对象指针
    }

    void deallocate(void* obj) {
        pool.push_back(obj); // 将对象放回池中
    }

private:
    std::vector<void*> pool; // 存储可用对象的内存池
};

// 使用示例
int main() {
    const size_t objectSize = 16; // 每个对象占用 16 字节
    ObjectPool<objectSize> pool(10); // 创建一个对象池,最多存储 10 个对象

    void* obj1 = pool.allocate(); // 从内存池中分配一个对象
    std::cout << "Allocated object at: " << obj1 << " with size: " << objectSize << " bytes." << std::endl;

    pool.deallocate(obj1); // 将对象释放回内存池
    return 0;
}

ObjectPool pool(10); 这行代码确实是创建了一个内存池的实例,负责管理最多可以存储 10 个大小为 objectSize 字节的对象。这种设计模式特别适合高性能场景下的内存管理。

这里的内存池为10个大小的objectSize 的对象,当内存池空间使用完了的话,可以抛出异常返回nullptr;也可以扩展内存池,内存池可以再分配一块新的内存区域,并将新内存区域的对象添加到池中;

模板参数实现定长内存池

内存池中的对象大小固定。使用模板类可以让我们在创建对象池时指定对象的类型,内存池将为该类型的对象提供内存管理。下面是一个简单的对象池实现示例,展示了如何通过模板来创建一个定长内存池。

#include <iostream>
#include <vector>
#include <stdexcept>

template <class T>
class ObjectPool {
public:
    ObjectPool(size_t size) : poolSize(size), freeList(size) {
        // 预分配内存并初始化空闲链表
        for (size_t i = 0; i < poolSize; ++i) {
            freeList[i] = new T();  // 分配对象
            available.push_back(freeList[i]);  // 将对象加入可用列表
        }
    }

    ~ObjectPool() {
        // 清理内存
        for (T* obj : freeList) {
            delete obj;
        }
    }

    T* allocate() {
        if (available.empty()) {
            throw std::runtime_error("No available objects in the pool");
        }
        // 从可用列表中取出一个对象
        T* obj = available.back();
        available.pop_back();
        return obj;
    }

    void deallocate(T* obj) {
        // 将对象放回可用列表
        available.push_back(obj);
    }

private:
    size_t poolSize;
    std::vector<T*> freeList;    // 存储预分配的对象
    std::vector<T*> available;    // 存储可用的对象
};

// 示例用法
int main() {
    try {
        ObjectPool<int> intPool(5);  // 创建一个对象池,最多存储 5 个 int 对象
        
        // 分配对象
        int* a = intPool.allocate();
        int* b = intPool.allocate();

        // 使用对象
        *a = 10;
        *b = 20;
        std::cout << "Allocated values: " << *a << ", " << *b << std::endl;

        // 释放对象
        intPool.deallocate(a);
        intPool.deallocate(b);

        // 再次分配对象
        int* c = intPool.allocate();
        std::cout << "Allocated value: " << *c << std::endl;

        // 清理
        intPool.deallocate(c);
    } catch (const std::runtime_error& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    }

    return 0;
}

使用模板参数的定长内存池(如 ObjectPool)的确只能管理一种特定类型的对象。每次创建一个对象池时,模板参数 T 决定了该内存池能够分配的对象类型。例如,如果你创建一个 ObjectPool,这个对象池就只能管理 int 类型的对象,而无法管理其他类型的对象。
可以通过继承多态实现多个对象类型的管理

如何直接向堆申请空间

malloc 确实是标准 C++ 库中用于动态内存分配的函数,通常它会在后台调用操作系统提供的底层内存分配函数(如 brk、sbrk 或 mmap),以便为程序分配内存。而在实现定制的内存池时,直接使用 mmap 或其他低级内存分配方法,可以获得更多控制权和灵活性。

malloc 和 mmap 的区别
分配方式:
malloc 通常使用堆(heap)来管理内存,堆的分配和释放通常需要维护一些元数据,这可能导致内存碎片。
mmap 直接向操作系统请求一块虚拟内存区域,可以更高效地处理大块内存请求,并且提供了灵活的内存管理策略。
用途:
malloc 适用于一般用途的动态内存分配。
mmap 更常用于需要大块内存、共享内存或需要精细控制内存管理的场景

#include <iostream>
#include <stdexcept>

#ifdef _WIN32
#include <Windows.h>
#else
#include <sys/mman.h>
#include <unistd.h>
#endif

// 直接去堆上申请按页申请空间
inline static void* SystemAlloc(size_t kpage) {
#ifdef _WIN32
    void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
    void* ptr = mmap(nullptr, kpage * 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (ptr == MAP_FAILED) {
        ptr = nullptr; // mmap失败处理
    }
#endif
    if (ptr == nullptr) {
        throw std::bad_alloc();
    }
    return ptr;
}

template <typename T>
class ObjectPool {
public:
    ObjectPool(size_t size) : poolSize(size) {
        // 向系统申请内存
        memory = SystemAlloc(size);
        // 进一步的初始化逻辑,例如构造对象
    }

    ~ObjectPool() {
        // 释放内存
        free(memory);
    }

private:
    size_t poolSize;
    void* memory; // 存储申请的内存地址
};

// 示例用法
int main() {
    try {
        ObjectPool<int> intPool(10); // 创建一个存储10个int的对象池
    } catch (const std::bad_alloc& e) {
        std::cerr << "Memory allocation failed: " << e.what() << std::endl;
    }

    return 0;
}

通过封装内存分配,您可以创建一个便于管理的对象池。无论是在 Windows 还是 Linux 平台,使用 SystemAlloc 函数都能简单地申请内存。此外,使用 try-catch 语句处理可能的内存分配异常,确保程序的健壮性。这样的设计模式不仅增强了代码的可移植性,还提高了内存管理的灵活性。

定长内存池实现代码

//定长内存池
template<class T>
class ObjectPool
{
public:
	//申请对象
	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*)malloc(_remainBytes);
				_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;
	}
	//释放对象
	void Delete(T* obj)
	{
		//显示调用T的析构函数清理对象
		obj->~T();

		//将释放的对象头插到自由链表
		NextObj(obj) = _freeList;
		_freeList = obj;
	}
private:
	char* _memory = nullptr;     //指向大块内存的指针
	size_t _remainBytes = 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和free
	size_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;
}

高并发内存池

现代很多的开发环境都是多核多线程,因此在申请内存的时,必然存在激烈的锁竞争问题。malloc本身其实已经很优秀了,但是在并发场景下可能会因为频繁的加锁和解锁导致效率有所降低,而该项目的原型tcmalloc实现的就是一种在多线程高并发场景下更胜一筹的内存池。

在实现内存池时我们一般需要考虑到效率问题和内存碎片的问题,但对于高并发内存池来说,我们还需要考虑在多线程环境下的锁竞争问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值