C++内存池(附源码)
前段时间阅读了Nginx的源码,其对内存高效的管理给我留下了深刻的印象,而内存管理的核心便是内存池。于是想自己实现一个C++版本的内存池,这方面当然还是STL的内存池最为经典,所以免不了参悟借鉴。内存池的概念早已经是老生常谈,然而把内存池实现的高效安全仍是个比较艰巨的问题。内存池的原理简单来讲就是一次性的向系统申请大量的内存,之后再有内存请求的时候,如果内存池的内存大小能够满足请求,就从内存池里分配,不必再进行系统调用,从而实现性能提升,而多次的内存申请系统调用,很容易生成内存碎片而造成内存浪费。池的概念大体如此,线程池,进程池无出其右。内存池的实现主要解决的问题有:
1. 内存池的块管理
2. 内存的分配和回收
3. 大块内存的分配和回收
4. 对象初始化
内存池的块管理
这方面可以直接参考STL的分配器的实现,SGI STL在进行内存分配时,默认使用了一个内存池。这个内存池的内存块从8Byte开始,每递增8Byte都生成一系列链表管理的内存块,一直到128Byte结束。内存块定义为:
union MemNode {
MemNode* _next;
char _data[1];
};
union每个成员的起始地址都是开头的位置,所以每次仅能使用一个成员,在链表中由_next指向下个内存块的地址,在分配内存时由_data指向内存首地址,长度为1 的数组放在结构体最后一个成员位置,可以访问给结构体多分配的地址空间,这种技术叫做柔性数组。这样做的好处减少了对内存块管理时额外的内存损耗。想想我们学习数据结构时实现的链表,都是通过结构体的一个成员来指向下个节点的地址,多出了一个指针4Byte的内存消耗。参考STL,我们内存块的管理如下图所示:
有同学要问了,那我要是申请比128更大的内存怎么办?SGI 这里就直接走正常的内存申请,还是会有系统调用产生。因为系统对于程序请求的内存,管理时也会生成额外的内存控制数据占用内存,这样申请的内存越小,额外占用的内存比例就越高。 我们每次申请指定量的内存,然后将内存格式化到块管理的数组链表中。
char* res;
size_t need_bytes = size * nums;
size_t left_bytes = _pool_end - _pool_start;
//内存池够用
if (left_bytes >= need_bytes) {
res = _pool_start;
_pool_start += need_bytes;
return res;
} else if (left_bytes >= size) {
nums = left_bytes / size;
need_bytes = size * nums;
res = _pool_start;
_pool_start += need_bytes;
return res;
}
size_t bytes_to_get = size * nums;
if (!is_large) {
if (left_bytes > 0) {
MemNode* my_free = _free_list[FreeListIndex(left_bytes)];
((MemNode*)_pool_start)->_next = my_free;
_free_list[FreeListIndex(size)] = (MemNode*)_pool_start;
}
} else {
free(_pool_start);
}
_pool_start = (char*)malloc(bytes_to_get);
//内存分配失败
if (0 == _pool_start) {
throw std::exception("There memary is not enough!");
}
_malloc_vec.push_back(_pool_start);
_pool_end = _pool_start + bytes_to_get;
return ChunkAlloc(size, nums, is_large);
将返回的内存添加到块管理队列中
my_free = &(_free_list[FreeListIndex(size)]);
*my_free = next = (MemNode*)(chunk + size);
for (int i = 1;; i++) {
current = next;
next = (MemNode*)((char*)next + size);
if (nums - 1 == i) {
current->_next = nullptr;
break;
} else {
current->_next = next;
}
}
内存的分配和回收
每次从系统申请内存时都通过一个辅助函数将内存增到为8的倍数,上层请求内存时寻找最小能容纳当前请求的头节点索引
//获取size最小8的倍数
size_t RoundUp(size_t size) {
return ((size + __align - 1) & ~(__align - 1));
}
//获取容纳当前size的最小内存块索引
size_t FreeListIndex(size_t size) {
return (size + __align - 1) / __align - 1;
}
当找到索引位置时,如果内存块不为空,则取出当前内存块,将之后的链表节点向前移动,如果内存不够的话,再次向系统请求新的内存。
std::unique_lock<std::mutex> lock(_mutex);
MemNode** my_free = &(_free_list[FreeListIndex(sz)]);
MemNode* result = *my_free;
if (result == nullptr) {
void* bytes = ReFill(RoundUp(sz));
memset(bytes, 0, sz);
return bytes;
}
*my_free = result->_next;
memset(result, 0, sz);
return result;
内存回收时与此理相同,通过辅助函数找到索引位置,将内存块放入首部位置,之前的内存块后移。
MemNode* node = (MemNode*)m;
MemNode** my_free = &(_free_list[FreeListIndex(len)]);
std::unique_lock<std::mutex> lock(_mutex);
node->_next = *my_free;
*my_free = node;
m = nullptr;
大块内存的分配和回收
通过以上的内存管理,我们足以解决小块内存的非配和回收,但是还可能存在另一种需求,类似Nginx内存池有大块内存的管理,我们在实际开发中也会用到诸如接收发送缓存之类的需求。这里添加增加一个新的类CBlockMemaryPool,以管理大块的内存块,而且要支持动态的增减:
通过一个vector来管理池中空闲的内存块,需要注意的是析构时要将所有从池中申请的内存还给内存池,不然就需要自己手动释放。申请内存块的生命周期管理可以交给智能指针。
对象初始化
与C语言实现内存池的不同之处在于,C语言可以只负责内存的分配而不用管内部数据的初始化,因为C语言没有对象的概念。但是在C++中,我们不仅仅要负责内存的分配,还要调用构造函数负责对象的初始化。大家知道C++中的 new操作符,一是负责内存申请,二是调用构造函数实现对象初始化。而C++中可以通过可变模板参数来实现任意数量任意参数的函数转发,再辅之std::forward完美转发,即可实现构造函数的调用功能。所以我实现的内存池对外提供内存申请的接口有三个:
//for object. invocation of constructors and destructors
template<typename T, typename... Args >
T* PoolNew(Args&&... args);
template<typename T>
void PoolDelete(T* &c);
//for continuous memory
template<typename T>
T* PoolMalloc(int size);
template<typename T>
void PoolFree(T* &m, int len);
//for bulk memory.
//return one bulk memory node
template<typename T>
T* PoolLargeMalloc();
template<typename T>
void PoolLargeFree(T* &m);
这样每次请求和释放都需要调用接口,C++对这种操作最熟悉不过,我们交给智能指针来管理即可。还有这里为什么没有重载new操作符来呢?因为new和delete的重载函数只能是static函数(因为new对象的时候,对象还没有创建),所以内存池的api通过重载new 和delete 实现,看起来很美好,但实际上是行不通的。我们要创建内存池的对象,每个内存池的对象管理的都是不同的内存。下面看下 PoolNew 调用构造函数的过程。
template<typename T, typename... Args>
T* CMemaryPool::PoolNew(Args&&... args) {
int sz = sizeof(T);
if (sz > __max_bytes) {
void* bytes = malloc(sz);
T* res = new(bytes) T(std::forward<Args>(args)...);
return res;
}
std::unique_lock<std::mutex> lock(_mutex);
MemNode** my_free = &(_free_list[FreeListIndex(sz)]);
MemNode* result = *my_free;
if (result == nullptr) {
void* bytes = ReFill(RoundUp(sz));
T* res = new(bytes) T(std::forward<Args>(args)...);
return res;
}
*my_free = result->_next;
T* res = new(result) T(std::forward<Args>(args)...);
return res;
}
到这里基本上所有的功能都已经实现完毕。但是既然我们支持创建内存池的对象,那什么时候释放内存池占有的内存呢? 当然是析构函数中! 但是怎么释放呢? 我们是通过malloc 库函数申请的内存,释放的时候自然是去调用free释放。但是我们不能通过循环块的数组和链表去释放内存。因为我们申请的时候是一整块去申请的,释放的时候只要通过每次申请的头地址去释放即可。所以我在这里添加了一个辅助的std::vector来存储每次申请内存的地址,释放的时候只遍历这个std::vector即可。
//声明
std::vector<char*> _malloc_vec;
//存储
_pool_start = (char*)malloc(bytes_to_get);
if (0 == _pool_start) {
throw std::exception("There memary is not enough!");
}
_malloc_vec.push_back(_pool_start);
//释放
for (auto iter = _malloc_vec.begin(); iter != _malloc_vec.end(); ++iter) {
if (*iter) {
free(*iter);
}
}
一些线程安全相关的内容没有在文章里提到,以上代码还没有经过充分的测试,如有错误的地方欢迎大家指出。
以下是使用实例:
class test1 {
public:
int aaaa;
int bbbb;
int cccc;
int dddd;
explicit test1(int a, int b, int c, int d):aaaa(a), bbbb(b), cccc(c), dddd(d){
std::cout << "test1()" << std::endl;
}
~test1() {
std::cout << "~test1()" << std::endl;
}
};
class test2 {
public:
int aaaa;
test2() {
std::cout << "test2" << std::endl;
}
~test2() {
std::cout << "~test2()" << std::endl;
}
};
int main() {
CMemaryPool pool;
test1* t1 = pool.PoolNew<test1>(1,2,3,4);
t1->aaaa = 1000;
t1->bbbb = 1000;
t1->cccc = 1000;
t1->dddd = 1000;
pool.PoolDelete<test1>(t1);
test2* t2 = pool.PoolNew<test2>();
t2->aaaa = 1000;
pool.PoolDelete<test1>(t1);
pool.PoolDelete<test2>(t2);
int len1 = sizeof(unsigned long);
int len2 = sizeof(char*);
int a;
std::cin >> a;
}
最后贴出源码地址
GitHub : https://github.com/caozhiyi/CppNet/blob/master/base/MemoryPool.h