目录
当前项目实现的高并发内存池原型是goole的开源项目tcmalloc,tcmalloc全称为Thread-Caching Malloc,即线程缓存的malloc,该项目实现了高效的多线程内存管理,用于替代系统的相关内存分配函数(malloc free)。
malloc实际就是一个内存池,为什么还要实现tcmalloc,原因时tcmalloc是用于优化C++写的多线程应用,比glibc.23的malloc块,这个模块可以用来让MySQL在高并发内存占用更加稳定(多线程环境下比malloc快)。
优点是相比malloc在多线程管理下能做到更高效的内存管理,替代malloc和free。
"池化技术",就是程序先向系统申请过量资源,然后交由自己管理。之所以要申请过量资源,是因为每次申请内存的时候会有较大的开销,提前申请好内存,需要时使用,可以大大的提高程序运行效率。
在计算机中,除了内存池,还有连接池、线程池、对象池等都用到了池化这种技术。以服务器上的线程为例,它的主要思想是:启动诺干数量的线程,让他们处于睡眠状态,当接收到客户端的请求时,唤醒池中某个睡眠的线程,让它来处理客户端的请求,当处理完这个请求,线程又进入睡眠状态。
内存池是指程序预先从操作系统中申请一块足够大的内存,此后,当程序中需要申请内存的时候,不是直接向操作系统申请,而是从内存池中直接获取。同时,当程序释放内存的时候,也不是真正将内存返回给操作系统,而是返回给内存池,当程序退出时,内存池才会将之前申请的大块内存真正释放。
1、效率问题
避免程序频繁向系统申请资源而产生较大的开销,自己提前申请足够内存供自己使用,可以提高程序运行效率。
2、内存碎片问题
在系统的内存分配器的角度,需要解决内存碎片问题,内存碎片有内碎片和外碎片两种。
内碎片问题是由于一些对齐需求,导致分配出去的空间中有一些内存无法被利用(部分小块空间利用不上)。
目前先介绍外碎片,所谓外碎片问题,是指一些空间的连续内存区域太小,这些内存空间不连续,以至于即使有足够的内存空间,也无法满足一些内存分配申请的需求(一段连续空间被切出去好多快,但是只有部分还回来了,但是它们不连续,导致要申请超过某个空间申请不出来,即使总空间大于要申请的空间,也申请不下来)。
C/动态申请内存都是通过malloc区堆上申请内存,C++借助new/delete去堆上申请释放内存空间,new和delete里面封装的是operator new和operator delete,operator new要符合C++抛异常机制里面封装了malloc,因此在C/C++中申请释放内存块,最终还是使用malloc和free的。
但是我们要知道,我们实际不是区堆上获取内存的。通过调用malloc函数,malloc调用brk()向操作系统申请内存,操作系统执行brk()系统调用,再将申请的内存通过malloc返回。
而malloc就是一个内存池。malloc相当于向操作系统申请了较大的内齿空间,然后零售给程序使用。当全部零售完或者程序需要大量的内存需求时,再根据实际需求向操作系统中申请"进货"内存。malloc的实现方式有多种方式,不同的编译器平台用的都是不同的。例如windows的vs系统用自己的微软写的一套,linux gcc用的glibc中的ptmalloc。
尺有所短,寸有所长,任何事物都有其优缺点,使用malloc申请内存是广泛应用于各种场景的,但是在什么场景下都可以使用,意味着什么场景下都不会有很高的性能,定长内存池是高并发内存池后续使用的基础组件,本先了解实现该内存池的实现
定长内存池可以满足固定大小的申请释放需求,特点是性能达到极致,不需要考虑内存碎片问题。
需要定义一个变量_memory指向申请的大块内存空间,当申请的固定大小内存用完返回后时候需要通过_freeList自由链表把还回来的内存块链接起来管理。
定长内存池可以通过malloc向系统申请空间,也可以直接向操作系统申请空间,window和Linux下可以直接向堆申请页为单位的大块内存。
windows用的接口是VirtuallAlloc,直接跳过malloc内存池,直接找堆按页为单位申请内存。
Linux通过调用brk()和nmap()向系统申请空间。
cLinux进程分配内存的两种方式--brk() 和mmap()
#pragma once
#include<iostream>
#include<vector>
#include<time.h>
#include<stdlib.h>
using std::cout;
using std::endl;
#ifdef _WIN32
#include<windows.h>;
#else
#endif
//直接去堆上按页申请空间
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
//kpage左移13位 为 128k
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;
//_freeList不为空 优先把还回来的内存块对象重复利用
if (_freeList)
{
//将自由链表头指针赋值给obj对象指针
//
obj = (T*)_freeList;
_freeList = *(void**)obj;
/* void* next = *((void**)_freeList);
obj =(T*)_freeList;
_freeList = next;*/
}
else
{
//剩余内存不够一个对象大小时,则重新开辟空间
//要被128*1024整除 刚好就是完整的对象
//没有被整除 剩余的一小块不够一个对象来用
//用_remainBytes<sizeof(T)来解决
if (_remainBytes < sizeof(T))
{
_remainBytes = 128 * 1024;
//_memory = (char*)malloc(_remainBytes);//128kb
//128k右移13位 为 16页
_memory = (char*)SystemAlloc(_remainBytes>>13);
if (_memory == nullptr)
{
printf("malloc fail!\n");
exit(-1);
}
}
//面临问题:
//内存块可能用完 New申请时需要判断
obj = (T*)_memory;
//保证可以存下下一个对象的地址
size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
_memory += objSize;
_remainBytes -= objSize;
/*_memory += sizeof(T);
_remainBytes -= sizeof(T);*/
}
//定位new,显示调用T的构造函数初始化
//对已经有的空间进行初始化
new(obj)T;
return obj;
}
void Delete(T* obj)
{
//显示调用析构函数清理对象
obj->~T();
//obj的头四个字节存放自由链表的头指针存
//更新头指针
*(void**)obj = _freeList;
_freeList = obj;
//if (_freeList == NULL)
//{
// _freeList = obj;
// *(void**)obj = nullptr;
//}
//else
//{
// //头插
// *(void**)obj = _freeList;
// _freeList = obj;
//}
}
private:
char* _memory=nullptr;//指向大块内存块的指针
size_t _remainBytes = 0;//大块内存块在切分过程中剩余的字节数
void* _freeList=nullptr;//还回来过程中链接的自由链表的头指针
};
#define _CRT_SECURE_NO_WARNINGS 1
#include"ObjectPool.h"
class TreeNode
{
public:
TreeNode() :_val(0), _left(nullptr), _right(nullptr)
{
}
int _val;
TreeNode* _left;
TreeNode* _right;
};
//测试定长内存池和new轮转释放内存效率
void PerformanceTest()
{
//申请释放的轮次
const size_t Rounds = 3;
//每轮申请释放的次数
const size_t N = 100000;
size_t begin1 = clock();
std::vector<TreeNode*> v1;
v1.reserve(N);
for (size_t i = 0; i < Rounds; 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();
ObjectPool<TreeNode> TNPool;
size_t begin2 = clock();
std::vector<TreeNode*> v2;
v2.reserve(N);
for (size_t i = 0; i < Rounds; i++)
{
for (size_t j = 0; j < N; j++)
{
v2.push_back(TNPool.New());
}
for (size_t j = 0; j < N; j++)
{
TNPool.Delete(v2[j]);
}
v2.clear();
}
size_t end2 = clock();
cout << "new cost time:" << end1 - begin1 << endl;
cout << "Object pool cost time:" << end2 - begin2 << endl;
}
int main()
{
PerformanceTest();
return 0;
}