什麼是記憶體池?
在上一篇 C++記憶體管理:new / delete 和 cookie 中談到,頻繁的呼叫 malloc 會影響執行效率以及產生額外的 cookie, 而記憶體池的思想是預先申請一大塊記憶體,當有記憶體申請需求時,從記憶體池中取出一塊記憶體分配給目標物件。
它的實現過程為:
- 預先申請 chunk 大小的記憶體池, 將記憶體池劃按照物件大小劃分成多個記憶體塊。
- 以連結串列的形式,即通過指標將記憶體塊相連,頭指標指向第一個空閒塊。
- 當有記憶體申請需求時,首先檢查頭指標是否指向空閒塊,如果是則將頭指標指向的第一個空閒塊分配出去(從連結串列移除),同時頭指標指向下一個空閒塊;若頭指標為空,說明當前記憶體池已分配完,需要重新申請新的記憶體池。
- 當有記憶體釋放需求時,將釋放的記憶體塊重新加入連結串列的表頭,調整頭指標指向新加入的空閒塊。這也意味著,如果申請了多個記憶體池,在記憶體釋放的過程中會慢慢的合併到一起。
初步實現
#include <iostream> using namespace std; class Screen { public: Screen(int x) : i(x) { }; int get() { return i; } void* operator new(size_t); void operator delete(void*, size_t); private: Screen* next; static Screen* freeStore; //頭指標 static const int screenChunk; //記憶體塊數量 private: int i; }; Screen* Screen::freeStore = 0; const int Screen::screenChunk = 5; void* Screen::operator new(size_t size){ Screen *p; if (!freeStore) { //記憶體池是空的 size_t chunk = screenChunk * size; freeStore = p = reinterpret_cast<Screen*>(new char[chunk]); for (; p != &freeStore[screenChunk - 1]; ++p) { //以連結串列的形式串聯起來 p->next = p + 1; } p->next = 0; } p = freeStore; freeStore = freeStore->next; return p; } void Screen::operator delete(void *p, size_t){ //將記憶體塊重新加入連結串列表頭,同時調整頭指標 (static_cast<Screen*>(p))->next = freeStore; freeStore = static_cast<Screen*>(p); } //------------- void test(){ cout << "Size: " << sizeof(Screen) << endl; size_t const N = 100; Screen* p[N]; for (int i = 0; i < N; ++i) p[i] = new Screen(i); for (int i = 0; i < 10; ++i) //輸出地址觀察 cout << i << ": " << p[i] << endl; for (int i = 0; i < N; ++i) delete p[i]; } int main(){ test(); return 0; }
在上面的程式碼中設定一個記憶體池為5個記憶體塊,當我們進行100次記憶體申請後,列印出前10個地址檢視,可以看到前5個地址是連續的,後5個也是連續的,但中間由於重新申請了記憶體池,所以不是連續的。
但是這樣的方法還存在著問題,那就是引入了額外的指標記憶體消耗,接下來將使用embeded pointer進行改進。
使用嵌入指標改進
上面就使用到了嵌入指標,一個 AirplaneRep 物件的大小為 8 位元組,而一個 Airplane 的指標大小為 4 位元組或 8 位元組。在 32 位機器下, 指標可以借用 AirplaneRep 物件所佔的 8 位元組記憶體空間中的前 4 個位元組,用來連線空閒的記憶體塊。而當記憶體塊需要被分配給物件時,此時它已從連結串列中移除,也就不需要指標來連線了。此時的 8 位元組記憶體空間由 AirplaneRep 佔據。當記憶體釋放時也是同理,由於 Rep 和 next 不會同時用到,所以 embeded pointer 的做法可以減少記憶體消耗。
參考: