C++內存管理:簡易內存池的實現


什么是內存池?

在上一篇 C++內存管理:new / delete 和 cookie 中談到,頻繁的調用 malloc 會影響運行效率以及產生額外的 cookie, 而內存池的思想是預先申請一大塊內存,當有內存申請需求時,從內存池中取出一塊內存分配給目標對象。

它的實現過程為:

  1. 預先申請 chunk 大小的內存池, 將內存池划按照對象大小划分成多個內存塊。
  2. 以鏈表的形式,即通過指針將內存塊相連,頭指針指向第一個空閑塊。
  3. 當有內存申請需求時,首先檢查頭指針是否指向空閑塊,如果是則將頭指針指向的第一個空閑塊分配出去(從鏈表移除),同時頭指針指向下一個空閑塊;若頭指針為空,說明當前內存池已分配完,需要重新申請新的內存池。
  4. 當有內存釋放需求時,將釋放的內存塊重新加入鏈表的表頭,調整頭指針指向新加入的空閑塊。這也意味着,如果申請了多個內存池,在內存釋放的過程中會慢慢的合並到一起。

 

初步實現

 

#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 中,每次還要寫一大堆重復的內容,於是可以使用宏進一步簡化:

 

 

 

 

 

 

 參考:

  1. 【C++內存管理】內存管理實例 (二) —— Embeded pointer

  2. 嵌入式指針embedded pointer的概念以及用法

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM