什么是內存池?
在上一篇 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個也是連續的,但中間由於重新申請了內存池,所以不是連續的。
但是這樣的方法還存在着問題,那就是引入了額外的指針內存消耗,接下來將使用embedded pointer進行改進。
使用嵌入指針改進
上面就使用到了嵌入指針,一個 AirplaneRep 對象的大小為 8 字節,而一個 Airplane 的指針大小為 4 字節或 8 字節。在 32 位機器下, 指針可以借用 AirplaneRep 對象所占的 8 字節內存空間中的前 4 個字節,用來連接空閑的內存塊。而當內存塊需要被分配給對象時,此時它已從鏈表中移除,也就不需要指針來連接了。此時的 8 字節內存空間由 AirplaneRep 占據。當內存釋放時也是同理,由於 Rep 和 next 不會同時用到,所以 embedded pointer 的做法可以減少內存消耗。
更簡化:static allocator
前面的實現需要為每個類都重寫 operator new 和 operator delete,由於內容是一樣的,使用另一個類來完成這些重復的操作。
如此一來,我們的 class 只需要去調用 allocator 即可完成內存的申請和釋放工作。
#include <iostream> #include <complex> using namespace std; namespace jj09{ class allocator{ private: struct obj { struct obj* next; //embedded pointer }; public: void* allocate(size_t); void deallocate(void*, size_t); void check(); private: obj* freeStore = nullptr; const int CHUNK = 5; }; void* allocator::allocate(size_t size){ obj* p; if (!freeStore) { size_t chunk = CHUNK * size; freeStore = p = (obj*)malloc(chunk); for (int i = 0; i < (CHUNK - 1); ++i) { p->next = (obj*)((char*)p + size); p = p->next; } p->next = nullptr; //last } p = freeStore; freeStore = freeStore->next; return p; } void allocator::deallocate(void* p, size_t){ ((obj*)p)->next = freeStore; freeStore = (obj*)p; } void allocator::check(){ obj* p = freeStore; int count = 0; while (p) { cout << p << endl; p = p->next; count++; } cout << count << endl; } //-------------- class Foo { public: long L; string str; static allocator myAlloc; public: Foo(long l) : L(l) { } static void* operator new(size_t size){ return myAlloc.allocate(size); } static void operator delete(void* pdead, size_t size){ return myAlloc.deallocate(pdead, size); } }; allocator Foo::myAlloc; class Goo { public: complex<double> c; string str; static allocator myAlloc; public: Goo(const complex<double>& x) : c(x) { } static void* operator new(size_t size){ return myAlloc.allocate(size); } static void operator delete(void* pdead, size_t size){ return myAlloc.deallocate(pdead, size); } }; allocator Goo::myAlloc; //------------- void test_static_allocator_3(){ Foo* p[100]; cout << "sizeof(Foo)= " << sizeof(Foo) << endl; for (int i = 0; i < 23; ++i) { //23,任意數, 隨意看看結果 p[i] = new Foo(i); cout << p[i] << ' ' << p[i]->L << endl; } //Foo::myAlloc.check(); for (int i = 0; i < 23; ++i) { delete p[i]; } //Foo::myAlloc.check(); { Goo* p[100]; cout << "sizeof(Goo)= " << sizeof(Goo) << endl; for (int i = 0; i < 17; ++i) { //17,任意數, 隨意看看結果 p[i] = new Goo(complex<double>(i, i)); cout << p[i] << ' ' << p[i]->c << endl; } //Goo::myAlloc.check(); for (int i = 0; i < 17; ++i) { delete p[i]; } //Goo::myAlloc.check(); } } } //namespace int main(void) { jj09::test_static_allocator_3(); return 0; }
macor for static allocator
在上面的 Foo 和 Goo 中,每次還要寫一大堆重復的內容,於是可以使用宏進一步簡化:
參考: