C++项目实战——高性能内存池(一)
目录
一、项目介绍
二、什么是内存池
1、池化技术
2、内存池
三、开胃菜——先设计一个定长的内存池
之前已经学习了很多C++的内容,那么接下来是时候用已学习的内容来做一个项目了,一方面可以巩固所学,另一方面可以将学习的内容结合起来学以致用。
一、项目介绍
当前项目是实现一个高并发内存池,其原型是Google的一个开源项目tcmalloc,tcmalloc全称是Thread-Caching Malloc,即线程缓存的malloc,实现了高效的多线程内存管理,用于替代系统的内存分配相关函数。我们接下来的做法就是将tcmalloc最核心的框架简化后拿出来进行模拟实现出一个自己的高并发内存池,通过这种方式来学习tcmalloc的精华。项目糅合了很多知识内容,包括C/C++、数据结构、操作系统内存管理、单例模式、多线程、互斥锁等等,代码会比较复杂且多个部分相互关联,我会将我理解和做出的内容进行详细地讲解并记录在这里,方便后续的回顾,如果有需要的小伙伴也可以跟着我一起来学习。让我们一起不怕困难,勇往直前!
二、什么是内存池
1、池化技术
“池化技术”就是程序先向系统申请过量的资源,然后自己管理,以备不时之需。之所以要申请过量的资源,是因为每次申请资源都会有较大的开销,如果提前申请好了,那么使用的时候就会非常快捷,提高了程序运行效率。
举个例子,当我们每天早上去上学时都去叫醒爸爸妈妈来领取当天的饭钱,那么爸爸妈妈每天早上都会被我们吵得睡不好觉,那么如果每个月初爸爸妈妈就把当月的所有饭钱给到我们,我们把这些钱放在钱包中,需要时就可以直接方便地进行使用而不需要再去找爸爸妈妈了。
2、内存池
内存池就是程序预先从操作系统申请一块足够大内存,此后,当程序中需要申请内存的时候,不是直接向操作系统申请,而是直接从内存池中获取;同理,当程序释放内存的时候,也是将内存返回内存池,而不是返回给操作系统。当程序退出时,内存池才将之前申请的内存真正释放。
三、开胃菜——先设计一个定长的内存池
我们先来尝试实现一个定长的内存池,熟悉一下内存池是如何控制的,同时也为后面的项目内容打下基础
我们首先需要向系统申请一段这样的大块空间,作为内存池,当我们需要进行使用时,直接从这段空间内进行拿取
当我们用完以后,需要返回的内存空间则以链表的形式把空间链接起来,这个链表我们就叫它自由链表
每个结点都为一段内存空间,每个结点中都使用了一个指针的大小来用于存放下一结点的地址,这样就可以链接起来了,那么我们在需要使用空间时,可以先使用被释放回来的链表中的空间,如果链表为空,就去内存池中进行申请空间,如果内存池中的空间不够了,就去系统中再去申请一块大的空间放到内存池。我们可以像图中一样,用两个指针来分别指向两部分,便于操作,这样一来我们可以写出代码,实现一个类
template<class T>
class objectpool
{
public://申请空间T* New(){//指向申请出的空间T* obj = nullptr;//如果自由链表上有内存,优先从自由链表上申请内存if (_freelist){obj = (T*)_freelist;//*(void**)是将void*指针先强转为二级指针,再取地址,就可以得到下一结点的地址_freelist = *(void**)_freelist;}//如果自由链表上没有可申请的空间,则从内存池中进行申请else{//如果内存池不为空,则直接拿取内存使用if (_memory){obj = (T*)_memory;_memory += sizeof(T);}//如果内存池为空,先要从系统中申请空间,再拿取使用else{_memory = (char*)malloc(128 * 1024);obj = (T*)_memory;_memory += sizeof(T);}}return obj;}//释放空间时直接将空间释放到自由链表上进行头插void Delete(T* obj){*(void**)obj = *(void**)_freelist;_freelist = (void*)obj;}private:char* _memory = nullptr;//指向内存池void* _freelist = nullptr;//指向自由链表};
这里我们使用了模板参数,即任何类型的对象都可以使用这个类来申请空间,由于指针的大小在32位系统下为4字节,而在64位系统下为8字节,那么如果该对象的大小较小,作为自由链表中的结点时不足以存放下一结点的指针怎么办?当我们在去内存池申请内存时,如果内存池中依然有空间,但不足以申请出一个对象,那么这里并不能去malloc,也不能使用剩余内存,又该如何呢?
由此看来,上面的这段代码还是有很多问题存在的,那么我们可以根据这些问题来对代码进行优化
一方面,我们可以控制从内存池上申请对象时,将类型的大小与指针大小进行比较,选择大的那个进行申请;另一方面,当内存池的内存不足一个对象的空间大小时就去向系统进行申请一份更大的空间,那么就需要增加一个变量用于存储剩余空间的字节大小
template<class T>
class objectpool
{
public://申请空间T* New(){//指向申请出的空间T* obj = nullptr;//如果自由链表上有内存,优先从自由链表上申请内存if (_freelist){obj = (T*)_freelist;//*(void**)是将void*指针先强转为二级指针,再取地址,就可以得到下一结点的地址_freelist = *(void**)_freelist;}//如果自由链表上没有可申请的空间,则从内存池中进行申请else{size_t objsize = sizeof(T) > sizeof(void*) ? sizeof(T) : sizeof(void*);//如果内存池空间不够,去系统中进行申请if (_remainsize < objsize){_memory = (char*)malloc(128 * 1024);if (_memory == nullptr)throw bad_alloc();_remainsize = 128 * 1024;}obj = (T*)_memory;_memory += objsize;_remainsize -= objsize;}//使用定位new对已经开辟的空间通过显式调用T的构造函数的方法来进行初始化new(obj)T;return obj;}//释放空间时直接将空间释放到自由链表上进行头插void Delete(T* obj){//显式调用析构函数obj->~T();*(void**)obj = *(void**)_freelist;_freelist = (void*)obj;}private:char* _memory = nullptr;//指向内存池void* _freelist = nullptr;//指向自由链表size_t _remainsize = 0;//内存池内剩余字节数
};
对于上面的内容,我们可以写一个程序来进行测试一下,假设有两个数组,定义一下树型结点,数组中的每个元素存放的都是一个结点,其中一个数组中每存放一个元素都使用new对象的方式,而另一个数组则采用我们刚才写出的申请内存的方式
struct TreeNode
{int _val = 0;TreeNode* left = nullptr;TreeNode* right = nullptr;
};
void Testobjpool()
{size_t Round = 5;size_t N = 10000;vector<TreeNode*> v1;v1.reserve(N);size_t begin1 = clock();for (size_t i = 0; i < Round; i++){for (size_t j = 0; j < N; j++){v1.push_back(new TreeNode);}for (size_t j = 0; j < N; j++){delete v1[j];}v1.clear();}size_t end1 = clock();cout << "New cost time" << ":" << end1 - begin1 << endl;vector<TreeNode*> v2;v2.reserve(N);objectpool<TreeNode> objtree;size_t begin2 = clock();for (size_t i = 0; i < Round; i++){for (size_t j = 0; j < N; j++){v2.push_back(objtree.New());}for (size_t j = 0; j < N; j++){objtree.Delete(v2[j]);}v2.clear();}size_t end2 = clock();cout << "objpool cost time" << ":" << end2 - begin2 << endl;
}
int main()
{Testobjpool();
}
通过运行打印可以看出,内存池的方式确实将效率提高了