一個非常簡單的C++記憶體池方案
在遊戲中頻繁使用new與delete將會導致效能的下降,還可能造成記憶體碎片。
使用以一個自定義的記憶體分配器將是很重要的。
建立一個通用又強大效率效能又高的記憶體分配器將是困難的,所以這個的分配器面向下面的情況使用:
1. 一開始就就申請,直到遊戲退出才釋放
這種情況我們無需做分配器內部的記憶體釋放,僅需將分配器本身釋放掉即可
2. 申請後立即釋放
當然不算是立即釋放,不然就沒用了。
這種情況一般是需要一個臨時緩衝區,或者使用一個臨時物件。
我們的記憶體可以記錄上次申請的情況,方便重複利用記憶體資源。
當然,我們還需要支援一個強大的功能——任意位元組對齊。
記憶體對齊對提高效能很有幫助,特別地,有些地方對位元組對齊有硬性要求。
比如SIMD(單指令流多資料流)操作矩陣或者向量時,就要求128位即16位元組對齊。
我們就僅需將這些資料排到前面即可。
特別地,如果使用繼承特別是繼承抽象類需要特別注意資料在記憶體的排列方法。
比如繼承一個抽象類,那麼在資料的前4位元組(32位程式)將會是虛表指標,需要16位元組對齊的話,
需要在前面寫一些有用沒用資料,比如指標啥的。
好了,放程式碼,不足百行
// 只申記憶體池 // EachPoolSize : 每片緩衝池的大小 略大於實際能分配記憶體數 template<size_t EachPoolSize=128 * 1024> class AllocOnlyMPool{ // 節點 struct Node{ // 後節點 Node* next = nullptr; // 已分配數量 size_t allocated = 0; // 上次分配位置 BYTE* last_allocated = nullptr; // 緩衝區 BYTE buffer[0]; }; public: // 建構函式 AllocOnlyMPool(){ assert(m_pFirstNode && "<AllocOnlyMPool::AllocOnlyMPool>:: null m_pFirstNode"); } // 解構函式 ~AllocOnlyMPool(); // 申請記憶體 void* Alloc(size_t size, UINT32 align = sizeof(size_t)); // 釋放記憶體 void Free(void* address); private: // 申請節點 static __forceinline Node* new_Node(){ auto* pointer = reinterpret_cast<Node*>(malloc(EachPoolSize)); pointer->Node::Node(); return pointer; } // 首節點 Node* m_pFirstNode = new_Node(); }; // **實現** // 申請記憶體 template<size_t EachPoolSize> void* AllocOnlyMPool<EachPoolSize>::Alloc(size_t size, UINT32 align){ if (size > (EachPoolSize - sizeof(Node))){ #ifdef _DEBUG assert(!"<AllocOnlyMPool<EachPoolSize>::Alloc>:: Alloc too big"); #endif return nullptr; } // 獲取空閒位置 auto* now_pos = m_pFirstNode->buffer + m_pFirstNode->allocated; // 獲取對齊後的位置 auto aligned = (reinterpret_cast<size_t>(now_pos)& (align - 1)); if (aligned) aligned = align - aligned; now_pos += aligned; // 增加計數 m_pFirstNode->allocated += size + aligned; // 檢查是否溢位 if (m_pFirstNode->allocated > (EachPoolSize - sizeof(Node))){ Node* node = new_Node(); if (!node) return nullptr; node->next = m_pFirstNode; m_pFirstNode = node; // 遞迴(僅一次) return Alloc(size, align); } // 記錄上次釋放位置 m_pFirstNode->last_allocated = now_pos; return now_pos; } // 釋放記憶體 template<size_t EachPoolSize> void AllocOnlyMPool<EachPoolSize>::Free(void* address){ // 上次申請的就這樣了 if (address && m_pFirstNode->last_allocated == address){ m_pFirstNode->allocated = (m_pFirstNode->last_allocated - m_pFirstNode->buffer); m_pFirstNode->last_allocated = nullptr; } } // AllocOnlyMPool 解構函式 template<size_t EachPoolSize> AllocOnlyMPool<EachPoolSize>::~AllocOnlyMPool(){ Node* pNode = m_pFirstNode; Node* pNextNode = nullptr; // 順序釋放 while (pNode) { pNextNode = pNode->next; free(pNode); pNode = pNextNode; } }
順序說明一下:
1. 使用模板,引數是每個單元的大小,因為儲存了資料,實際每個單元能夠分配的記憶體量略小,
看實際情況賦予引數吧,128k一般夠了。大不了上兆,反正記憶體白菜價了。
模板還可能增加目標程式大小,可改為變數。但是一般用一個模板示例就行了,就無所謂了
2.Node使用空陣列(中括號裡面啥也沒有),標準C++支不支援我不知道,反正VC++警告了”非標準擴充套件“,
不過空陣列是在C99裡面允許的
3.對指標行進位操作進行對齊操作,雖說是任意位元組對齊。
但是一般得低於4k,因為一般作業系統一頁的大小為4k,高於4k將毫無意義。
使用方法: 例項化物件。 需要全域性使用的話就用靜態變數。
多執行緒支援。
遊戲一般將會是多執行緒的,所以應該對多執行緒進行支援。
但是上鎖與解鎖需要時間,違背的這個記憶體池的設計初衷。
所以一般遊戲推薦的是“無鎖操作”,所以在這就不對其進行上鎖。
那怎麼保證安全呢?
我們僅需保證對其進行互斥操作——為每個需要這個記憶體池的執行緒分配一個記憶體池即可
這就是物件記憶體池的好處