定長內存池的設計
- 定長內存池
- 定長內存池的原理講解
- 代碼實現
- 定義對象
- New對象的主要邏輯
- delete對象的主要邏輯
- 完整代碼
定長內存池
為什么我們要設計這個定長內存池呢?首先malloc是c標準庫中向堆申請空間的接口,變相的說malloc是普遍性,而我們要實現的高并發內存池是特殊性,是針對某種情況下把效率提高到極致的一種申請空間的方法。在后續我們的項目中也會用到定長內存池,所以定長內存池的設計有兩方面,一是讓我們熟悉一下內存的分配問題,二是他會作為我們的高并發內存池的一個基礎組件。
定長內存池的原理講解
首先我們需要在內存中申請一塊固定大小的空間,如下圖:
后面我們考慮如何分配空間以及管理后續的空間。這里我們像系統申請了一大塊空間,如果再有用戶需要申請空間這個時候我們就可以直接從我們管理的那一大塊空間中取出空間,從而避免了重復申請和釋放空間的效率問題。但是這里用戶歸還的空間我們也不能直接還給操作系統,所以我們需要使用一個freelist(自由鏈表)將歸還的空間管理起來
每次從上面取完空間后,歸還空間我們就直接頭插到鏈表中。下次如果在需要申請,我們就可以直接使用_freelist中的空間直接給到用戶使用。
代碼實現
定義對象
我們先定義一個class ObjectPool類對象
#pragma once
#include<iostream>
using std::endl;
using std::cout;template<class T>
class Object {
public:
private:char* _memory = nullptr; //指向內存池的起始位置size_t _remain = 0; //內存剩余空間大小void* _freelist = nullptr; //指向自由鏈表的第一個節點的指針
};
這里定義三個私有成員變量,具體用處見上述代碼注釋。
New對象的主要邏輯
首先我們對于空間申請的類,肯定就是new 和 delete兩個重要的接口。我們先來寫new:
T* New()
{T* obj = nullptr; //先頂一個返回的objif (_freelist) //如果自由鏈表不為空,此時我們直接使用自由鏈表的空間{//這里就相當于鏈表的頭刪void* next = *(void**)_freelist; //定義一個next存儲鏈表下一個節點的地址 obj = (T*)_freelist; //再讓obj指向分配內存的首地址_freelist = next; //再將自由鏈表連接到下一個節點處}else{if (_remain<sizeof(T)) //如果內存空間剩余大小小于當前類型的大小{//重新像系統申請一塊大空間_remain = 128 * 1024; //更新內存剩余空間大小_memory = (char*)malloc(_remain); //開辟空間if (_memory == nullptr) //如果_memory為空則報錯{throw std::bad_alloc();}}obj = (T*)_memory; //obj接收被切割的首地址size_t objsize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T); //這里為了防止開辟的空間沒有辦法儲存地址,所以如果類型大小小于地址的大小,則開辟地址大小字節的空間,否則開辟類型大小的空間。_memory += objsize; //隨后_memory指向后面的空間_remain -= objsize; //_remain減少//顯示調用一下構造,應對string等這些類型的構造new(obj)T;return obj;}
}
delete對象的主要邏輯
delete就相當于是鏈表的頭插
void Delete(T* obj)
{obj->~T();*(void**)obj = _freelist;_freelist = obj;
}
這里的主要邏輯就結束了,接下來對代碼進行優化一下。
我們這里使用的還是malloc,所以我們還可以完全脫離malloc使用系統調用接口virtualalloc這個接口直接在堆上申請空間,接下來就是完整代碼,還有一段測試的例子我們可以清楚的看到我們的定長內存池在申請同一類型的空間時效率的提升有多夸張。
完整代碼
這里我們使用了條件編譯用來區分windows和Linux中不同的系統調用接口。
#pragma once
#include<iostream>
#include<vector>using std::cout;
using std::endl;#ifdef _WIN32
#include<windows.h>
#else
//
#endif
// 直接去堆上按頁申請空間
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else// linux下brk mmap等
#endifif (ptr == nullptr)throw std::bad_alloc();return ptr;
}template<class T>
class ObjectPool{
public:T* New(){T* obj = nullptr;if (_freelist)//如果自由鏈表不為空則使用自由鏈表中的空間{void* next = *(void**)_freelist;obj = (T*)_freelist;_freelist = next;}else {if (_remain < sizeof(T)){_remain = 128 * 1024;//_memery = (char*)malloc(_remain);_memery = (char*)SystemAlloc(_remain>>13);if (_memery == nullptr){throw std::bad_alloc();}}obj = (T*)_memery;size_t objsize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);_memery += objsize;_remain -= objsize;}new(obj)T;return obj;}void Delete(T* obj){obj->~T();*(void**)obj = _freelist;_freelist = obj;}private:char* _memery = nullptr;//內存池起始地址size_t _remain = 0; //內存池剩余字節數void* _freelist = nullptr; //自由鏈表起始地址
};struct TreeNode
{int _val;TreeNode* _left;TreeNode* _right;TreeNode():_val(0), _left(nullptr), _right(nullptr){}
};void TestObjectPool()
{// 申請釋放的輪次const size_t Rounds = 5;// 每輪申請釋放多少次const size_t N = 100000;std::vector<TreeNode*> v1;v1.reserve(N);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();std::vector<TreeNode*> v2;v2.reserve(N);ObjectPool<TreeNode> TNPool;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;
}
Debug:
Release: