C + + memory management summary

Why memory management?

The size of this application and related debugging cookie s will be saved in the memory applied by malloc. This information is redundant when continuously applying for memory, because the memory size of each object applied is the same; Malloc will call the system call to request memory from the operating system, which involves context switching. Therefore, the purpose of our memory management:

  • Minimize the number of malloc
  • Reduce the requested memory cookie size

1. Basic components provided by C + + for memory management

operator new/operator delete

prototype   
void* operator new(size_t size)   // Size is the memory size to be requested (bytes)
void operator delete(void* deadObj, size_t size)

malloc/free

void* malloc(size_t size)
void free(void* deadObj)

placement new/placement delete
Note: placement new does not apply for memory, but does the constructor operation on the applied memory. Only when the call to placement new fails, the corresponding placement delete will be called to deal with the construction failure

use
void* p = operator new(sizeof(int));
int* pi = static_cast<int*>(p);
new(p)int(10);     

1.1 operation behind calling new and delete

struct Foo{
	Foo(int _i):i(_i){}
	int i;
}
int* p = new Foo(10);

Int * P = mechanism behind new foo (10)

1. void* mem = operator new(sizeof(Foo));
2. Foo* pc =  static_cast<Foo*>(mem);
3. pc->Foo::Foo(10);            // This can be called under vc, and gnuc will fail
   new(pc)Foo(10)   // placement new can also be used

Specific steps of operator new operation

The main action is malloc. When malloc fails, that is, oom, call its own new handler to deal with the failure scenario. Possible implementation: release the temporarily unused memory and let this call return the available memory.
new handler has two specific purposes: 1. Make more memory available; 2. Call abort() or exit()

2. Implement per class allocator yourself

2.1 version1

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;
};
void* Screen::operator new(size_t size){
	Screen* p;
	if (!freeStore) {
		size_t chunk = screenChunk*size;
		// Request a chunk of memory
		freeStore = p = reinterpret_cast<Screen*>(new char[chunk]); 
		// Connect small pieces of memory with a linked list
		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);
}

Screen* Screen::freeStore = 0;
const int Screen::screenChunk = 24;

2.2 version2

Improvement: the embedded pointer is added to save the cost of the next pointer, because the next pointer will only be used when allocating memory and reclaiming memory, and only the content of the object will be used when returning to the user instead of the next pointer, so the union is used to wrap the next pointer and class object

class Airplane {
private:
	struct AirplaneRep {
		unsigned long miles;
		char type;
	};
private:
	union {
		AirplaneRep rep;
		Airplane* next;
	};
public:
	unsigned long getMiles() { return rep.miles; }
	char getType() { return rep.type; }
	void set(unsigned long m, char t)
	{
		rep.miles = m;
		rep.type = t;
	}
public:
    // You can add static here or not. The compiler will add it to us because the class object is not constructed and its behavior is unknown.
	static void* operator new(size_t size);    
	static void operator delete(void* deadObj, size_t size);
private:
	static const int BLOCK_SIZE;
	static Airplane* headOfFreeList;
};
Airplane* Airplane::headOfFreeList;  // Idle chain header    
const int Airplane::BLOCK_SIZE = 512; // The number of blocks requested at one time when the free linked list is empty

void* Airplane::operator new(size_t size){
	// Inheritance will lead to unequal size. Don't think too much here
	if (size != sizeof(Airplane))
		return ::operator new(size);

	Airplane* p = headOfFreeList;
	if (p)
		headOfFreeList = p->next;
	else {
		Airplane* newBlock = static_cast<Airplane*>
		(::operator new(BLOCK_SIZE*sizeof(Airplane)));
		// String the requested memory with a linked list
		for (int i = 1; i < BLOCK_SIZE - 1; ++i)
			newBlock[i].next = &newBlock[i + 1];
		newBlock[BLOCK_SIZE - 1].next = 0;
		p = newBlock;
		headOfFreeList = &newBlock[1];
	}
	return p;
}

void Airplane::operator delete(void* deadObj, size_t size) {
	if (deadObj == 0) return;
	if (size != sizeof(Airplane)) {
		::operator delete(deadObj);
		return;
	}
	//Recovering the object, the pointer's next points to the free chain header, and then adjusts the free chain header to deadObj.
	(static_cast<Airplane*>(deadObj))->next = headOfFreeList;
	headOfFreeList = static_cast<Airplane*>(deadObj);
}

2.3 abstract as allocator

Configure an allocator for each class

class myAllocator
{
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; //Smaller for easy observation 
};

void* myAllocator::allocate(size_t size)
{
	obj* p;

	if (!freeStore) {
		//The linked list is empty, so grab a large piece of memory
		size_t chunk = CHUNK * size;
		freeStore = p = (obj*)malloc(chunk);

		//cout << "empty. malloc: " << chunk << "  " << p << endl;

		//Connect the allocated large block in series as a linked list
		for (int i = 0; i < (CHUNK - 1); ++i) {  //It's beautiful if it's not written. It doesn't matter if it's not the point  
			p->next = (obj*)((char*)p + size);
			p = p->next;
		}
		p->next = nullptr;  //last       
	}
	p = freeStore;
	freeStore = freeStore->next;

	//cout << "p= " << p << "  freeStore= " << freeStore << endl;

	return p;
}
void myAllocator::deallocate(void* p, size_t)
{
	//Insert the deleted object retraction into the front end of the free list
	((obj*)p)->next = freeStore;
	freeStore = (obj*)p;
}

Use of myAllocator

class Foo {
public:
	long L;
	string str;
	static myAllocator myAlloc;  // Macro
public:
	Foo(long l) : L(l) {  }
	static void* operator new(size_t size)		 // Macro
	{ return myAlloc.allocate(size); }		 
		static void  operator delete(void* pdead, size_t size)  // Macro
	{
		return myAlloc.deallocate(pdead, size);
	}
};
myAllocator Foo::myAlloc;		// Macro

2.4 Macro replacement

Write the reusable code in version 3 into a macro

3.GNU2.9 alloc implementation details

The first level allocator (> 128 bytes) directly calls malloc
The secondary allocator (< = 128 bytes) calls the overloaded operator new() in alloc


Explanation:
Suppose the application object size is n.

  1. First, the alloc works on all objects. The size of the applied object is n rounded up to a multiple of 8
  2. Maintain a memory pool. When the memory pool is empty, apply for N202 + Roundup (cumulative application amount).
    Return one piece of memory to the user. The remaining 19 pieces are connected with a linked list, and the remaining 20*n size is used as the memory pool
  3. Each of the 16 linked lists maintains a free linked list. When the empty free linked list is empty, first consider cutting out the desired size (up to 20) in the memory pool. If there is not enough, malloc a large block and repeat step 2.

Tags: C++ memory management

Posted on Wed, 29 Sep 2021 23:42:02 -0400 by Ted Striker