Jump to content
  • Advertisement
Sign in to follow this  
schupf

Memory Manager

This topic is 2698 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

Hello!

I want to write a little game for windows and a console and someone recommended to avoid scattered heap allocation all over the place. Instead I should centralize all allocation at one place: a memory manager class.

So instead of allocating directly:
Foo* f = new Foo(2);
I would do this:
Foo* f = MemoryMng.allocate(??, sizeof(Foo), "an foo object");

I especially like the idea with the string. He recommended to add meta info like a description string to every allocated memory block since this would be very helpful for debugging purposes. For example if a level is finished and there are still memory blocks labeled with "Level..." floating around then its quite clear there is a memory leak.

My questions are:

1) Do you think such a global memory manager is a good idea?

2) I have some problems implementing such a class. Mainly I don't know what I should pass the allocate() method to tell them what class (or block or array or whatever) he should allocate.

For example lets say I want to allocate a char array and an object of class Foo. For objects I could use a string and each class had to register a creation method, so calls would look like this:
Foo* f = MemoryMng.allocate("Foo", "an foo object");

But how should I allocate an array this way?

My second thought was a template method like this: Foo* f = MemoryMng.allocate<Foo>("my foo object")

But how should I pass parameters to the Foo constructor with this method? Any ideas how I could implement such an allocate method?

3) Where should I store the meta information (like the descriptive string)? For example I want to connect the string "level data" to a memory block of 1000 short and a string "my object" to a foo object. How
could i do this?




Thanks for any help!

Share this post


Link to post
Share on other sites
Advertisement

1) Do you think such a global memory manager is a good idea?
Definately. Every commercial game engine I've used has had a centralised memory tracker akin to this.
As well as collecting information on leaks, you can gather all sorts of useful statistics, like how much memory each of your subsystems are using, or even export the state of all allocations into a GUI app for visualisation of your heap, etc...
2) I have some problems implementing such a class. Mainly I don't know what I should pass the allocate() method to tell them what class (or block or array or whatever) he should allocate. My second thought was a template method like this ... But how should I pass parameters to the Foo constructor with this method?[/quote]The template method looks good.
You can have it simply allocate the memory, but not call the constructor for you, and then use placement new to call the constructor yourself.
Foo* f = MemoryMng.allocate<Foo>("my foo object");
f = new (f) Foo( a, b, c );
MemoryMng.deallocate(f);//calls f->~Foo();
Alternatively, you can continue doing memory allocations using new/delete as you always have, but override the global new/delete operators, like:class MyGlobalAllocator
{
public:
static void* Allocate(size_t size, const char* tag);
static void Deallocate(size_t size, void* data);
};

void* operator new(size_t size)
{
return MyGlobalAllocator::Allocate(size, "");
}
void* operator new(size_t size, const char* tag)
{
return MyGlobalAllocator::Allocate(size, tag);
}//N.B. also need versions of new[]


void operator delete(void* allocation, size_t size)
{
return MyGlobalAllocator::Deallocate(size, allocation);
}
//N.B. also need delete[]

Another alternative is to only override new/delete on a class by class basis. One trick I've seen it to use a common base class for all engine allocated classes, and also define your own "new" keyword via a macro like this:

class MyGlobalAllocator
{
public:
static void* Allocate(size_t size, const char* tag, const char* file, int line);
static void Deallocate(size_t size, void* data);
};

#define MyNew(tag) new("tag", __FILE__, __LINE__)

class MyBaseClass
{
public:
void* operator new(size_t size, const char* tag, const char* file, int line)
{
return MyGlobalAllocator::Allocate(size, tag, file, line);
}
void operator delete(void* allocation, size_t size)
{
return MyGlobalAllocator::Deallocate(size, allocation);
}
//n.b. also need new[] and delete[]
private:
void* operator new(size_t size); // please use MyNew instead of new to create instances of this class
};

class MyGameObject : public MyBaseClass {};

void test()
{
MyGameObject* p1 = MyNew("tag") MyGameObject();//records the tag, file name and line number!
delete p1;
MyGameObject* p2 = new MyGameObject();//won't compile -- have to use MyNew instead of new
}
Also, if you don't want the allocator to be a static/global object, and want to use instances of it like in your example, you could re-write the above like this:

class MyAllocator
{
public:
static void* Allocate(size_t size, const char* tag, const char* file, int line);
static void Deallocate(void* data);
};

#define MyNew(a, tag) new(a, "tag", __FILE__, __LINE__)
#define MyDelete(a, obj) if(obj){obj->Delete(a);}

class MyBaseClass
{
public:
void* operator new(size_t size, MyAllocator& alloc, const char* tag, const char* file, int line)
{
return alloc.Allocate(size, tag, file, line);
}
void Delete(MyAllocator& alloc)
{
return alloc.Deallocate(this);
}
private:
void* operator new(size_t size); // please use MyNew instead of new to create instances of this class
void operator delete(void* allocation);//please use MyDelete instead of delete //n.b. also need new[] and delete[]
};

class MyGameObject : public MyBaseClass {};

void test()
{
MyAllocator alloc;
MyGameObject* p1 = MyNew(alloc, "tag") MyGameObject();//records the tag, file name and line number!
MyDelete(alloc, p1);
MyGameObject* p2 = new MyGameObject();//won't compile -- have to use MyNew instead of new
delete p2;//compile error, must use MyDelete instead of delete
}
3) Where should I store the meta information (like the descriptive string)? For example I want to connect the string "level data" to a memory block of 1000 short and a string "my object" to a foo object. How could i do this?[/quote]A common way is to allocate more than the request ammount, and either store this meta-data before or after the user's actual allocation.
Memory tracking is an extra expense too, so usually you'll want some kind of #ifdef that allows you to enable/disable this feature in different builds.
Here's a quick example of how I'd approach it:#define ENABLE_MEMORY_TRACKING

class MyAllocator
{
#ifdef ENABLE_MEMORY_TRACKING
struct AllocInfo
{
unsigned int debugMagic1;
const char* tag;
const char* file;
int line;
int size;
unsigned int debugMagic2;
};
const static unsigned magic = 0x13371337;
#endif
public:
static void* Allocate(size_t size, const char* tag, const char* file, int line)
{
#ifdef ENABLE_MEMORY_TRACKING
AllocInfo* allocation = (AllocInfo*)malloc(size + sizeof(AllocInfo));
allocation->debugMagic1 = allocation->debugMagic2 = magic;
allocation->size = size;
allocation->tag = tag;
allocation->file = file;
allocation->line = line;
void* data = allocation + 1; // equivalent to: ((byte*)allocation) + sizeof(AllocInfo);
unsigned int* footer = (unsigned int*)(((char*)data)+size);
*footer = magic;

//perhaps add allocation to a linked list or something here?

return data;
#else
return malloc(size);
#endif
}
static void Deallocate(void* data)
{
#ifdef ENABLE_MEMORY_TRACKING
AllocInfo* allocation = ((AllocInfo*)data) - 1;// equivalent to: ((byte*)data) - sizeof(AllocInfo)
//check that the header is intact, if this fails you've overwritten some memory
assert(allocation->debugMagic1 == magic);
assert(allocation->debugMagic2 == magic);
//check that the footer is intact, if this fails you've overwritten some memory
unsigned int* footer = (unsigned int*)(((char*)data)+allocation->size);
assert(*footer == magic);

//perhaps remove allocation from the above mentioned linked list or something here?

free(allocation);
#else
free(data);
#endif
}
};
N.B. none of this code's been tested.

Share this post


Link to post
Share on other sites

1) Do you think such a global memory manager is a good idea?
[/quote]
This has some pros and cons. Hodgman points out some of the pros. Centralizing stuff makes it a lot easier to track your memory issues. But I'm going to present some counterpoints. You can keep your centralized allocator, but it shouldn't be your only allocator.

1) A global memory manager is bad in the face of threading. It provides a singular location that has to be synchronized when different threads need to allocate something. And, with anything C++ish, you may have a bunch of hidden allocations that you didn't even consider. These are all going to serialize your code, ruining any parallel gains. If you do any theading, you'll want a more thread friendly allocator setup.

2) A global memory manager is only going to allocate memory in one way. That isn't always what you need. Some areas of your code will benifit from a generalized heap, others a fixed sized block allocator, and others just a fixed sized arena. All of these allocators can be initalized from the core allocator, but from there they will dole out that preallocated block in different ways.

3) A global memory manager is only going to allocate one type of memory. Some consoles have multiple memory types. But even without that complexity, it is nice to treat memory as if it was several blocks of different types. You can achieve much of this through point #2, but the idea is to coalesce similar types of data into contiguous blocks. Often called a "Data Oriented Design", you'd make sure data that will be used together is allocated together. Not to be confused with data that is related to other data, but stuff that is actually used together. A "Monster" has graphics, physics, and AI state. While all 3 are related, they aren't used together. You want to update all physics stuff together and update all drawing together and all ai together. You don't interleave those operations, so don't interleave the data. Let each type of data get allocated in a block of memory specific to stuff in that subsystem.

Share this post


Link to post
Share on other sites
Thanks for your answers! I am not sure which approach I should take. I really like both the global Allocator class/placement new and overriden global operator new approach.

Two more questions: In Hodgman's code about the meta info he added a footer after the allocated memory. Why did he do that?

I am a fan of smart pointers but I kinda think they don't fit into the concept of a global allocator. I mean if I allocate everything with one manager I guess the manager should also be the place where all delete calls are made? But then I had to implement some sort of MemoryMng.delete(f) method and I had to call this method on thousand places which is quite the opposite to the automatic deletion by smart pointers.

To put a long story short: Is there a way to add smart pointers into the concept of a global allocator class?

Share this post


Link to post
Share on other sites

In Hodgman's code about the meta info he added a footer after the allocated memory. Why did he do that?
[/quote]
To detect memory corruption. You can see the relevant assert() in the dellocation function.


To put a long story short: Is there a way to add smart pointers into the concept of a global allocator class?
[/quote]
Most smart pointers allow you to manage the deallocation yourself. So you use the smart pointer to inform you when to deallocate, and then a custom deallocation function is called. See boost::shared_ptr/std::shared_ptr as an example.

Share this post


Link to post
Share on other sites

Most smart pointers allow you to manage the deallocation yourself. So you use the smart pointer to inform you when to deallocate, and then a custom deallocation function is called. See boost::shared_ptr/std::shared_ptr as an example.

I am not sure what you mean with this. I have an intrusive smart pointer, so the reference counter is in the object itself. To be more specific: I have a base class for all my referenced classes which has a reference counter (int variable) and methods incRef() and decRef(). decRef() decrements the reference count and if it hits zero, delete this is called.

My smart pointer just calles pointee->incRef() in the Ctor and pointee->decRef() in the destructor. You mean I should just replace the delete this inside Object::decRef() with something like MemoryMng.deallocate(this)?


Share this post


Link to post
Share on other sites

I have an intrusive smart pointer, so the reference counter is in the object itself. To be more specific: I have a base class for all my referenced classes which has a reference counter (int variable) and methods incRef() and decRef(). decRef() decrements the reference count and if it hits zero, delete this is called.

My smart pointer just calles pointee->incRef() in the Ctor and pointee->decRef() in the destructor. You mean I should just replace the delete this inside Object::decRef() with something like MemoryMng.deallocate(this)?


In this case, when decRef() detects that the reference count is 0, it should manually call the destructor of the object and then do MemoryMng.deallocate(this).

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!