池化技術
????????所謂的池化技術,就是程序預先向系統申請過量的資源,然后自己管理起來,以備不時之需。這個操作的價值就是,如果申請與釋放資源的開銷較大,提前申請資源并在使用后并不釋放而是重復利用,能夠提高程序運行效率和減少開銷。
? ? ? ? 在計算機領域,池化技術有非常多的應用場景,如內存池、連接池、線程池和對象池等。以服務器中的線程池為例,它的主要思想是:預先啟動一批線程,讓它們先進入睡眠狀態,當有客戶端請求到來時,喚醒一個線程進行處理,并在處理完請求后,繼續睡眠,等待下一次被喚醒。
什么是定長池
? ? ? ? 我們C語言中使用的malloc實際上是標準庫的函數,底層實現實際上就使用了內存池技術,它支持根據我們的需求分配不同大小的內存空間,而我們今天要設計的定長池則每次只能分配固定大小的空間,在頻繁申請大小相同的空間的情況下,效率比malloc更優秀。
系統調用
? ? ? ? 在windows環境下進行開發,所以使用windows內存申請的API:
VirtualAlloc
在進程的虛擬地址空間中分配或保留內存
#include <windows.h>LPVOID VirtualAlloc
(LPVOID IpAddress, // 要分配的內存區域的地址SIZE_T dwSize, // 分配的大小DWORD flAllocationType,// 分配的類型DWORD flProtect // 該內存的初始保護屬性
);
參數解釋:
lpAddress:指定要分配的內存區域的起始地址。如果此參數為nullptr,則系統會自動決定分配內存區域的位置,并且按64KB向上取整。
dwSize:指定要分配或保留的區域的大小,以字節為單位。系統會根據這個大小一直分配到下頁的邊界。
flAllocationType:指定分配類型,可以是指定或合并以下標志:
- MEM_COMMIT:為指定地址空間提交物理內存。
- MEM_RESERVE:保留指定地址空間,不分配物理內存。這樣可以阻止其他內存分配函數(如malloc和LocalAlloc等)再使用已保留的內存范圍,直到它被釋放。
- MEM_TOP_DOWN:在盡可能高的地址分配內存。
- MEM_LARGE_PAGES:分配內存時使用大頁面支持。大小和對齊必須是一個大頁面的最低倍數。
flProtect:指定被分配區域的訪問保護方式。可能的值包括:
- PAGE_READWRITE:區域可以執行代碼,應用程序可以讀寫該區域。
- PAGE_READONLY:區域為只讀。如果應用程序試圖訪問區域中的頁,將會被拒絕訪問。
- PAGE_NOACCESS:任何訪問該區域的操作將被拒絕。
- PAGE_GUARD:區域第一次被訪問時進入一個STATUS_GUARD_PAGE異常,這個標志要和其他保護標志合并使用。
- PAGE_NOCACHE:RAM中的頁映射到該區域時將不會被微處理器緩存(cached)。
返回值:
如果函數調用成功,則返回分配的首地址;
如果調用失敗,則返回nullptr。可以通過GetLastError函數來獲取錯誤信息。
// 直接去堆上按頁申請空間
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32// 8K一頁為單位向操作系統申請內存空間void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else// linux下brk mmap等
#endifif (ptr == nullptr)throw std::bad_alloc();return ptr;
}static void*& NextObj(void* obj)
{return *(void**)obj;
}
定長池設計
一個分配固定大小內存的模板類,由于申請的內存大小固定,所以申請固定大小的空間時,性能比malloc更好一些,目前暫時不考慮內存碎片問題。
管理的成員:
-
預先申請的內存空間的指針memory
-
管理用戶釋放空間的空閑鏈表的指針freelist
-
空閑鏈表連接的方式是:用戶將內存釋放后,該內存空間的前4/8字節(取決于系統位數)空間用于存放下一塊內存空間的地址。
-
插入新空間到空閑鏈表:通過頭插法實現,由于系統位數不確定,所以使用二級指針解引用來獲得指針的大小,從而可以適用于32/64位系統。
-
-
記錄空間剩余大小的字段remain_size
template<class T>
class ObjectPool
{
private:char* _memory = nullptr; // 預先申請的內存空間size_t _remain_size = 0; // 剩余空間大小void* _freelist = nullptr; // 管理用戶釋放空間的空閑鏈表
};
提供的方法:
-
New:用戶申請空間的接口。
-
如果剩余空間大小不足一個空間,則重新開辟一塊新的固定大小的內存空間。
-
使用定位new,顯示調用構造函數后返回對象指針給用戶
-
更新memory指針偏移量和remain_size大小,如果T類型大小不足以存放下一塊空間的地址,則更新大小應為指針的大小。
-
T* New()
{T* obj = nullptr;// 優先使用空閑鏈表中的空間if (_freelist != nullptr){void* next = *((void**)_freelist);obj = (T*)_freelist;_freelist = next;}else{// 當空間不足時,開辟固定大小的空間if (_remain_size < sizeof T){_remain_size = 128 * 1024;//_memory = (char*)malloc(128 * 1024); // 定長池,開辟128KB的內存空間_memory = (char*)SystemAlloc(128 * 1024); // 定長池,開辟128KB的內存空間if (_memory == nullptr){throw std::bad_alloc();}}obj = (T*)_memory;// 分配的空間的大小至少要能夠存放下一塊空間的地址size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);_memory += objSize; // 分配定長空間后,指針向后偏移_remain_size -= objSize; // 更新剩余空間大小}// 定位new,顯式調用T的構造進行初始化new(obj)T;return obj;
}
-
Delete,釋放T*類型的指針指向的空間,但不會返回給操作系統,而是通過空閑鏈表管理起來。
-
顯式調用析構函數清理指針指向對象的資源,并將空閑空間頭插到空閑鏈表。
-
空閑鏈表連接的方式是:用戶將內存釋放后,該內存空間的前4/8字節(取決于系統位數)空間用于存放下一塊內存空間的地址。
-
插入新空間到空閑鏈表:通過頭插法實現,由于系統位數不確定,所以使用二級指針解引用來獲得指針的大小,從而可以適用于32/64位系統。
-
// 將用戶要釋放的空間用空閑鏈表管理起來
// 空閑鏈表連接的方式是:空間的前4/8字節(其實就是指針的大小,具體取決于系統位數)存放下一塊空間的地址
void Delete(T* obj)
{obj->~T();// 使用二級指針獲取指針,頭插法將空間添加到空閑鏈表*(void**)obj = _freelist;_freelist = obj;
}
性能測試
接下來我們對定長池進行性能測試,并與malloc進行比較,以下是測試代碼:
void TestPool()
{const int round = 5;const int times = 50000;std::vector<TreeNode*> v1;v1.reserve(5);size_t begin1 = clock();for (size_t j = 0; j < round; ++j){for (int i = 0; i < times; ++i){v1.push_back(new TreeNode);}for (int i = 0; i < times; ++i){delete v1[i];}v1.clear();}size_t end1 = clock();ObjectPool<TreeNode> TNPool;std::vector<TreeNode*> v2;v2.reserve(50000);size_t begin2 = clock();for (int i = 0; i < round; i++){for (int j = 0; j < times; j++){v2.push_back(TNPool.New());}for (int j = 0; j < times; j++){TNPool.Delete(v2[i]);}v2.clear();}size_t end2 = clock();std::cout << "malloc耗時:" << end1 - begin1 << std::endl;std::cout << "ObjectPool耗時:" << end2 - begin2 << std::endl;
}
Debug版本的比較:
Release版本的比較:
可以發現,在高頻分配固定大小對象的場景下,定長池的效率要比malloc更高,這是因為:
定長池只用于分配固定大小的對象,每次開辟的都是固定大小的內存塊,管理空閑空間也只需要使用簡單的空閑鏈表就能完成;而malloc需要處理各種各樣的場景,根據用戶需要分配不同大小的內存塊,空閑空間的管理也要復雜得多。