Skip to main content
GameDev.net gamedev.net
🔒 Locked

Memory Management/Object Pool/Recycle Old Memory

Started by MilfredCubicleX Aug 8, 2006 at 9:21 AM 7 replies 1.3k views
Original Post
MilfredCubicleX
MilfredCubicleX
Helloooo, This is not really a question, I actually want to discuss. I was reading the Enginuity article and such and decided to write the memory manager using some of the ideas in the forum topic about the article series. I'm specifically referring the idea of reuse of old objects from the dead list. So i wrote some code, tested it out, and I wanted to share the results. Basically, the code overrides the new operator and fetches a pointer from a dead list if there is one instead of allocating memory. When an object is deleted it puts the pointer on the dead list. The driver program creates an STL list of pointers and loops through randomly choosing to either allocate memory and add it to this list, or delete memory from the list. The driver program also has a timer to see how fast this is happening. My first iteration was disappointing because i chose to use STL lists for the alive and dead lists which made the overloaded new and delete run about twice as slow as an unoverloaded new and delete. So I went back and implemented my own linked list functionality, and got a nice speed improvement. I found in my tests that the overridden new and delete run about 33% faster than normal allocation. Anyway, I hope that makes sense, heres the code:

#include <cstdlib>
#include <conio.h>
#include <iostream>
#include <list>
#include <windows.h>

class Base 
{
private:
	static Base * s_AliveFront;
	static Base * s_DeadFront;
	Base * m_Prev, * m_Next;
public:
	Base( ) 
	{ 
	}
	~Base ( )
	{
	}
	inline static void * operator new ( size_t size )
	{
		Base * mem;
		// if dead list is not empty
		if( s_DeadFront )
		{
			mem = s_DeadFront;
			DeadListPopFront( );
		}
		// otherwise create new memory
		else
		{
			mem = (Base *)::operator new( size );
		}
		AliveListPushFront( mem );
		return mem;

	}
	inline static void operator delete ( void * p )
	{
		Base * mem = static_cast< Base * >( p );
		
		AliveListErase( mem );
		DeadListPushFront( mem );
	}
	inline static void AliveListPushFront( Base * p )
	{
		if( s_AliveFront )
		{
			p->m_Next = s_AliveFront;
			s_AliveFront->m_Prev = p;
			s_AliveFront = p;
			s_AliveFront->m_Prev = 0;
		}
		else
		{
			s_AliveFront = p;
			s_AliveFront->m_Next = 0;
			s_AliveFront->m_Prev = 0;
		}
	}
	inline static void DeadListPushFront( Base * p )
	{
		if( s_DeadFront )
		{
			p->m_Next = s_DeadFront;
			s_DeadFront->m_Prev = p;
			s_DeadFront = p;
			s_DeadFront->m_Prev = 0;
		}
		else
		{
			s_DeadFront = p;
			s_DeadFront->m_Next = 0;
			s_DeadFront->m_Prev = 0;
		}
	}
	inline static void DeadListPopFront( )
	{
		if( s_DeadFront )
		{
			if( s_DeadFront->m_Next )
			{
				s_DeadFront = s_DeadFront->m_Next;
				s_DeadFront->m_Prev = 0;
			}
			else
			{
				s_DeadFront = 0;
			}
		}
	}
	inline static void AliveListErase ( Base * p )
	{
		if( p->m_Next )
		{
			p->m_Next->m_Prev = p->m_Prev;
		}

		if( p->m_Prev )
		{
			p->m_Prev->m_Next = p->m_Next;
		}
		else
		{
			if( p->m_Next )
			{
				s_AliveFront = p->m_Next;
				s_AliveFront->m_Prev = 0;
			}
			else
			{
				s_AliveFront = 0;
			}
		}
	}
	static void Cleanup ( )
	{
		Base * ptr = s_AliveFront;
		if( ptr )
			while( ptr->m_Next )
			{
				ptr = ptr->m_Next;
				::operator delete( ptr->m_Prev );
			}
		s_AliveFront = 0;

		ptr = s_DeadFront;
		if( ptr )
			while( ptr->m_Next )
			{
				ptr = ptr->m_Next;
				::operator delete( ptr->m_Prev );
			}
		s_DeadFront = 0;
	}
	static void StatusReport ( )
	{
		Base * ptr = s_AliveFront;
		int count = 0;
		
		if( ptr )
			while( ptr->m_Next )
			{
				count++;
				ptr = ptr->m_Next;
			}
		std::cout << "Alive: " << count << std::endl;

		ptr = s_DeadFront;
		count = 0;
		
		if( ptr )
			while( ptr->m_Next )
			{
				count++;
				ptr = ptr->m_Next;
			}
		std::cout << "Dead: " << count << std::endl;
	}
};

Base * Base :: s_AliveFront = 0;
Base * Base :: s_DeadFront = 0;

int main(int argc, char *argv[])
{
	LARGE_INTEGER beforeTime, afterTime;
	std::list< Base * > baseList;
	int count = 0, totalTime = 0;
	
	srand( time( NULL ) );
	
	for( int i = 0; i < 500; i++ )
		baseList.push_back( new Base ( ) );
		

	while( !kbhit() )
	{
		QueryPerformanceCounter( &beforeTime );
		for( int i = 0; i < 500; i++ )
			for( int j = 0; j < 500; j++ )
				for( int k = 0; k < rand() % 5; k++ )
					if( rand() % 2 == 0 )
					{
						baseList.push_back( new Base ( ) );
					}
					else
					{
						if( !baseList.empty() )
						{
							Base * b = baseList.back( );
							delete b;
							baseList.pop_back( );
						}
						else
						{
							baseList.push_back( new Base ( ) );
						}
					}
		QueryPerformanceCounter( &afterTime );
		totalTime += afterTime.LowPart - beforeTime.LowPart;
		count++;
		std::cout << "Average Time: " << (int)( (float)totalTime / count ) << std::endl;
	}
	
	Base::StatusReport( );
	Base::Cleanup( );
	Base::StatusReport( );
	
    system("PAUSE");
    return EXIT_SUCCESS;
}

(To compare with regular allocation, just comment out the new operator new and operator delete) I wrote it in Dev-Cpp but i'm pretty sure it should also run in MSVC. In conclusion, thanks Superpig for the idea and such. Any thoughts? Ideas for optimization?
Extrarius
Extrarius
If you want a memory pool for something that is created and destroyed often (such as particles or projectiles, perhaps), I'd strongly suggest using Boost::Pool.

I can't guarantee that it will be faster that your implemented, but I'd bet that is the case, as boost is generally very, very well optimized. It's also well tested, and large parts of it will be in the next C++ standard, so it's a good idea to use it when it does something you need done =-)

A nice thing about Boost::Pool is that it's completely separate from the type of object you're dealing with, which means you can easily vary the allocation type for each container, such as having a linked list of short-lived particles in one spot that uses pooled memory and a vector of long-lasting particles allocated normally in another place.
"Walk not the trodden path, for it has borne it's burden." -John, Flying Monk
Enigma
Enigma
Quote:
Original post by Extrarius
I can't guarantee that it will be faster that your implemented, but I'd bet that is the case, as boost is generally very, very well optimized.

It is, by around 20-40% depending on compiler.

Σnigma
The C modest god
The C modest god
What I dont get is how are you going to allocate memory to different classes?
Are you going to use tempalte? or write this code for every class you want to do this?
It's all about the wheel.Never blindly trust technoligy.I love my internal organs.Real men don't shower. Quote: Original post by Toolmaker Quote:
Enigma
Enigma
Quote:
Original post by MilfredCubicleX
:-(
So much for learning

I'm sure you did learn something whilst implementing that. So you didn't manage to beat code written and peer reviewed by experts? Big deal. It's quite common for programmers to learn how something works then go and use a professional implementation.

By the way, you have a very minor memory leak in your implementation. You never delete the very last item in each list in your Cleanup function (and you don't count it either when you print the counts).

Σnigma
MilfredCubicleX
MilfredCubicleX
Ah, thanks. Indeed.

For an actual question though: Would you/do you use this type of system? Is the small speed improvement worth it? I actually thought it would be alot faster but oh well.
deffer
deffer
You can't do this:
mem = (Base *)::operator new( size );
and act on mem pointer inside new operator assuming its members will not be overwritten by some derived class constructor. You can't be sure of the actual alignment of Base members in the memory chunk that you just allocated.

For example, the following code blows on MSVC 7.1:
class Derived   : public Base{public:   virtual void Func() {};};Derived *pDer = new Derived;pDer->Func();delete pDer;

That's because the vtable pointer is the first member in the memory chunk, not the Base::m_Prev.

What you could do to addres this issue, is to move the Base members just before the memory chunk returned by the overloaded new. So the Base members will be completely hidden, nobody will know there really are Base::m_Prev and Base::m_Next members somewhere in the allocated chunk.
(That's how most compilers hide the vtable.)

class Base {   friend class Base2;   // everything as before, but no new/delete overloads};class Base2{public:   static void * operator new ( size_t size )   {      Base * mem;      // if dead list is not empty      if( Base::s_DeadFront )      {         mem = Base::s_DeadFront;         Base::DeadListPopFront( );      }      // otherwise create new memory      else      {         mem = (Base *)::operator new( size+sizeof(Base) );      }      Base::AliveListPushFront( mem );      return ((char*)mem) + sizeof(Base);   };   static void operator delete ( void * p )   {      Base * mem = static_cast<Base*>((void*)( ((char*)p)-sizeof(Base) ));      Base::AliveListErase( mem );      Base::DeadListPushFront( mem );   }};class Derived   : public Base2 // Base2 here!!!!!!1omg!{public:   virtual void Func() {};};

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.