为了学习池化技术以及后续自行实现一个仿tcmalloc的线程池,我们先浅浅的学习一下池化的概念,以及简单的实现一个定长的内存池。
一:池化技术
所谓“池化技术”,就是程序先向系统申请过量的资源,然后自己管理,以备不时之需。之所以要申请过量的资源,是因为每次申请该资源都有较大的开销,不如提前申请好了,这样使用时就会变得非常快捷,大大提高程序运行效率。在计算机中,有很多使用“池”这种技术的地方,除了内存池,还有连接池、线程池、对象池等。
以服务器上的线程池为例,它的主要思想是:先启动若干数量的线程,让它们处于睡眠状态,当接收到客户端的请求时,唤醒池中某个睡眠的线程,让它来处理客户端的请求,当处理完这个请求,线程又进入睡眠状态。
二:内存池
内存池是指程序预先从操作系统申请一块足够大内存,此后,当程序中需要申请内存的时候,不是直接向操作系统申请,而是直接从内存池中获取;同理,当程序释放内存的时候,并不真正将内存返回给操作系统,而是返回内存池。当程序退出(或者特定时间)时,内存池才将之前申请的内存真正释放。
三:内存池主要解决的问题
- 效率问题
- 内存碎片问题
再需要补充说明的是内存碎片分为外碎片和内碎片,上面这张图描述的是外碎片问题。
💡外部碎片是一些空闲的连续内存区域太小,这些内存空间不连续,以至于合计的内存足够,但是不能满足一些的内存分配申请需求。
💡内部碎片是由于一些对齐的需求,导致分配出去的空间中一些内存无法被利用。
四:malloc
C/C++中我们要动态申请内存都是通过malloc去申请内存,但是我们要知道,实际我们不是直接去堆获取内存的,而malloc就是一个内存池。malloc() 相当于向操作系统“批发”了一块较大的内存空间,然后“零售”给程序用。当全部“售完”或程序有大量的内存需求时,再根据实际需求向操作系统“进货”。malloc的实现方式有很多种,一般不同编译器平台用的都是不同的。
五:牛刀小试-定长内存池
⭐️特点:
- 性能达到极致
- 不考虑内存碎片问题
作为程序员(C/C++)我们知道申请内存使用的是malloc,malloc其实就是一个通用的大众货,什么场景下都可以用,但是什么场景下都可以用就意味着什么场景下都不会有很高的性能,下面我们就先来设计一个定长内存池做个开胃菜。
#include<iostream>
#include<vector>
#include<time.h>
using std::cout;
using std::endl;
//条件编译,兼顾跨平台
#ifdef _WIN32
#include<Windows.h>
#else
// linux
#endif
// 直接去堆上按页申请空间
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对象大小
T* obj = nullptr;
//优先把还回来的内存块对象再次重复利用
if (_freeList)
{
//自由链表头删
void* next = *((void**)_freeList);
obj = (T*)_freeList;
_freeList = next;
}
else
{
// 当剩余的内存不够一个对象的大小时,就重新开大块空间,剩余的就不要了,内碎片
if (_remainBytes < sizeof(T))
{
_remainBytes = 128 * 1024;
_memory = (char*)SystemAlloc(_remainBytes >> 13);//直接从堆区获得内存
if (_memory == nullptr)
{
throw std::bad_alloc();
}
}
//memory里面来切一块
obj = (T*)_memory;
size_t objSize = sizeof(T) < sizeof(T*) ? sizeof(void*) : sizeof(T);//按情况给多大的大小
_memory += objSize;
_remainBytes -= objSize;
}
//定位new,对已存在的空间显式调用T的构造函数初始化
new(obj)T;
return obj;
}
void Delete(T* obj)
{
//显式调用析构函数清理对象
obj->~T();
//头插
*(void**)obj = _freeList;//int**都可以,给个二级指针就可以
_freeList = obj;
}
private:
size_t _remainBytes = 0; //大块内存在被切分过程中剩余字节数
char* _memory = nullptr; //指向大块内存的指针
void* _freeList = nullptr; //还回过程中链接的自由链表指针
};
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 = 10000;
size_t begin1 = clock();
std::vector<TreeNode*> v1;
v1.reserve(N);
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;
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();
}
size_t end2 = clock();
cout << "new cost time:" << end1 - begin1 << endl;
cout << "object pool cost time:" << end2 - begin2 << endl;
}
测试结果:
new cost time:28
object pool cost time:14
请按任意键继续. . .
可以看出来,针对定长的空间申请,我们设计的定长内存池可以有明显的效率提升!
备注:如果大家对代码的细节有疑惑的地方,欢迎大家评论区提出,我们一起探讨!