Noticealso that simply allocating from the pool returns an object that may have beenused previously and so contains ‘garbage’: no constructor will have been called.This motivates the use
Trang 1template<class T>
T * mem_Pool<T>::Allocate(){
m_FreeItems.push( pItem );
}template<class T>
void mem_Pool<T>::FreeAll(){
Notice that the pool class allocates an array of objects dynamically This meansthat any class that requires pooling must provide a default constructor Noticealso that simply allocating from the pool returns an object that may have beenused previously and so contains ‘garbage’: no constructor will have been called.This motivates the use of the placement form of operator new: this calls a con-structor on an existing memory block:
class Duck{
public:
enum Gender { DUCK, DRAKE };
enum Breed { MALLARD, POCHARD, RUDDY, TUFTED };
Duck( Breed eBreed, Gender eGender );
// etc
Trang 2{
mem_Pool<Duck> s_DuckPool( 100 );
}
Duck * pDuck = s_DuckPool.Allocate();
// Call placement new on our raw data
new (pDuck) Duck( MALLARD, DRAKE );
Some ad hoc tests on my PIII 500 laptop indicate the pool class to be about ten
times faster than malloc() The class is extremely simple and robust, and does
not demand the sort of person-power resources that a global allocator does
Furthermore, plumbing it in to your code is as simple as adding operator new
and delete for each class to be pool allocated:3
// Thing.hpp
class Thing
{
public:
void * operator new( size_t n );
void operator delete( void * p );
};
// Thing.cpp
namespace
{
const int MAX_THINGS = 100;
mem_Pool<Thing> s_Pool( MAX_THINGS );
Trang 3A really dull application for pools
In the course of development, you’ll write some really fun code and you’ll writesome really bland stuff Annoyingly, a lot of how your game performs may well
be dictated by how well the dull stuff runs
There isn’t much duller stuff than linked lists Yet, if you need a data ture where you can add and remove items in constant time, you’d behard-pushed to find a better container class The temptation to use STL’sstd::list class is overwhelming, but a cursory investigation of any of thecommon implementations will yield the slightly disturbing realisation that afair number of calls to operators newand deletetake place during addandremove operations Now, for some purposes that may not matter – if you doonly one or two per game loop, big deal But if you’re adding and removing lots
struc-of things from lists on a regular basis, then newand deleteare going to hurt
So why are newand deletebeing called? If you require objects to be in eral lists at once, then the link fields need to be independent of the object in anode class And it’s the allocation and freeing of these nodes that can cause theinvocation of dynamic memory routines
sev-The thought occurs: why not write an allocator for nodes that uses ourpools, and plug it into the STL list class? After all, they provide that nice littlesecond template argument:
template<class T,class A = allocator<T> >
class list { /*…*/ };
Brilliant! Except for the fact that it really doesn’t work very well It depends toocritically on which particular version of STL you are using, so although yoursolution may possibly be made to work for one version of one vendor’s library,
it is quite unportable across versions and platforms
For this reason, it’s usually best to write your own high-performance tainer classes for those situations (and there will be many in a typical game)where STL will just not cut the mustard But we digress
con-Asymmetric heap allocationImagine an infinite amount of RAM to play with You could allocate and allo-cate and allocate, and not once would you need to free Which is great, becauseit’s really the freeing that catalyses the fragmentation process
Now, it’ll be quite a while before PCs and consoles have an infinite quantity
of free store However, the only constraint you need be concerned with is not
an infinite amount but just all that your game requires In fact, we can be a bitmore specific than that, because if the game is level-based, and we know ahead
of time exactly how much we need for a level, then we can pre-allocate it at thestart of the level, allocate when required, and the only free we will ever need isthe release of everything at the end of the level
Trang 4This technique generalises to any object or family of objects whose exactmaximum storage requirements are known ahead of time The allocator – called
an asymmetric heap because it supports only allocation of blocks, not freeing bar
the ability to free everything – is an even simpler class to implement than the
mem_AsymmetricHeap( int iHeapBytes );
// Constructs a heap using a pre-allocated block// of memory
mem_AsymmetricHeap( char * pMem, int iHeapBytes );
~mem_AsymmetricHeap();
// The required allocate and free methods
void * Allocate( int iSizeBytes );
void FreeAll();
// Various stats
int GetBytesAllocated() const;
int GetBytesFree() const;
Trang 5mem_AsymmetricHeap::~mem_AsymmetricHeap(){
if ( m_bDeleteOnDestruct ){
delete [] m_pData;
}}
void * mem_AsymmetricHeap::Allocate( int iSize ){
void * pMem = 0;
// Ensure we have enough space left
if ( m_pNextAllocation + iSize < m_pEndOfData ){
pMem = m_pNextAllocation;
m_pNextAllocation += iSize;
}return( pMem );
}
void mem_AsymmetricHeap::FreeAll(){
// You can’t get much faster than this!
m_pNextAllocation = m_pData;
Trang 6The remaining methods, along with the required bullet-proofing, and other
bells and whistles such as alignment, are left as exercises for the reader
Control your assets
Having said all I have about fragmentation, you may be forgiven for thinking
that I’m a bit blasé about it You’d be wrong In some applications, there comes a
time when fragmentation doesn’t just slow things down, it actually brings down
the whole house of cards Witness this error message from a recent project:
Memory allocation failure
Requested size: 62480
Free memory: 223106
Fragmentation has left memory in such a state that there is more than enough
RAM free; it’s just formed of itty bitty blocks of memory that are no use on
their own
Time to panic? Not quite yet The first step is to get rid of anything inmemory that is no longer needed until there is enough RAM The scary bit
comes when you realise that the error message appeared after we’d done that
Time to panic now? No, we’ll take a deep breath and hold on a momentlonger The next step to take here is for some sort of block merging to be per-
formed To be able to perform this efficiently, internally your memory allocator
ideally needs to support relocatable blocks Now, to be able to do this, the access
to the things that can be relocated needs to be controlled carefully, because
simply caching a pointer could lead you to accessing hyperspace if the object is
moved Using some kind of handle will solve this problem – we will discuss
han-dles shortly
This motivates us to control our game resources carefully in databases
Within these databases, we can shuffle and shunt around the memory-hogging
resources at will without adversely affecting the remainder of the game
We’ll look at the asset management issue in some detail in a later chapter
Strings
If there is one class above all that is going to fragment your memory, then it is
probably going to be the string In fact, you don’t even need to have a class to
do it; just use a combination of char *’s, functions such as strdup() and
free(), and you are almost just as likely to suffer
What is it about strings that hammers allocators? Well, for starters, they areusually small and they are all different sizes If you’re malloc()’ing and free()’
ing lots of one- and two-byte strings, then you are going to tax the heap
man-ager beyond breaking point eventually
But there is a further – related – problem Consider the programmer whowrites a string class because they hate having to use ugly functions such as
strcmp(), strdup()and strcpy(), and having to chase down all those string
Trang 7pointer leaks So they write a string class, and this allows such semantic ness as:
neat-String a = "hello";
String b = a;
Now there are (at least) two schools of thought as to what that looking =should do One says that the request is to copy a string, so let’s do justthat Schematically:
innocent-strcpy( b.m_pCharPtr, a.m_pCharPtr );
However, the second school says that this is wasteful If bnever changes, whynot just sneakily cache the pointer?
b.m_pCharPtr = a.m_pCharPtr;
But what happens if bchanges? Well, we then need to copy ainto band update
b This scheme – called copy-on-write (COW) – is implemented by many stringclasses up and down the land It opens up the can of worms (which is whatCOW really stands for) inside Pandora’s box, because the string class suddenlybecomes a complex, reference-counting and rather top-heavy affair, rather thanthe high-performance little beast we hoped for
So if we’re to avoid COW, we are then forced into using strings that forciblyallocate on creation/copy/assignment, and free on destruction, including pass-ing out of scope In other words, strings can generate a substantial number ofhits to the memory management system(s), slowing performance and leading tofragmentation Given that C++ has a licence to create temporary objects when-ever it feels like it, the tendency of programmers who know how strings behave
is to forgo using a string class and stick with the combination of static arraysand the C standard library interface Unfortunately, this does reject some of thepower that can be derived from a string class For example, consider a hash tablethat maps names to objects:
template<class Key,class Type>
class hash_table{
// Your implementation here
};
namespace{
hash_table<char *,GameObject *> s_ObjectMap;
Trang 8The problem with this is that if the hash table uses operator==to compare
keys, then you will compare pointers to strings, not the strings themselves One
possibility is to create a string proxy class that turns a pointer into a comparable
object – a ‘lite’ or ‘diet’ string, if you will:
class string_proxy
{
public:
string_proxy(): m_pString(0){
}
string_proxy( const char * pString ): m_pString( pString )
{}
bool operator==( const string_proxy & that ) const{
return(!strcmp( m_pString, that.m_pString ));
Trang 9strcpy( cBuffer, pText );
s_strText = string_proxy( cBuffer );
}
void bar(){
int x;
// Some stuff involving x
}
void main(){
}}
This will – at least some of the time – print ‘no!’ and other times may print ‘yes!’
or perhaps just crash Why? Because cBufferis allocated on the stack, and thecall to bar()can overwrite the region where the text was, and the string proxy
is pointing to Boy, did I have fun with those bugs!
We are left with the distinct impression that a string class is still the least bad
of several evils How might we go about creating one that doesn’t fragment, isn’tslower than a snail on tranquillisers and doesn’t leave us with dangling pointers?One way – a poor way – is to create strings with fixed-size buffers of themaximum expected string length:
class string{
public:
// A string interface here
private:
enum { MAX_CAPACITY = 256; }char m_cBuffer[ MAX_CAPACITY ];
Trang 10The trouble with this is that it is very wasteful: if most strings are shortish – say
16–32 characters long – then we are carrying around a lot of dead storage space
It might not sound much, but you could well be scrabbling around for that
space at the end of the project
Our preferred solution is to use a series of pool allocators for strings Startingwith a minimum string buffer length (say 16), we allocate a pool for 16-character-
length strings, another for 32-character strings, and so on up to a maximum string
length By allowing the user to control how many strings of each length can be
allocated, we can bound the amount of memory we allocate to strings, and we
can customise the sizes according to the game’s string usage pattern
Strings then allocate their buffers from the pools As they grow, they cate from the pool of the smallest suitable size that can contain the new length
allo-Since the strings use pool allocators, no fragmentation of string memory occurs,
and the allocation and free processes are very fast And there are no pointer
issues to fret over Job done
The moral of the tale is this: prefer local object allocation strategies to global ones
In the end, our game should be a collection of loosely coupled components This
degree of independence means that if we solve the memory allocation issues
within the components, then we get the global level pretty much for free Hurrah!
But – and there’s always a but – some people insist that this isn’t the whole
story And they have a point They say that there are reasons why you may take
the step of writing a global allocator early on It’s just that those reasons have
nothing to do with performance or fragmentation In fact, given that your
allo-cation strategies will depend critically on your pattern of usage, you will need
some sort of mechanism to instrument the existing allocator so that you can
find out what those patterns are Calls to newand deleteshould be logged,
allowing you to trace leaks and multiple deletions and also to build up a picture
of how your game uses heap memory
These are fine motivations However, they should be evaluated in the text of the priorities of the project, and they may do more harm than good
con-Consider the following code skeleton:
// Memory.hpp
#if defined( DEBUG )
#define MEM_LogNew( Class )\
new( FILE , LINE ) Class
// Provide a new ‘new’ that logs the line and file the
// memory was allocated in
Trang 11Every time you need to create a new object and keep a record of its creation(and presumably deletion), you need to include memory.hpp That creates one
of those dependency things that is going to hamper reusability If a colleaguefeels like using one of your components but has already written a differentmemory management system, then they’re going to have to modify the code.Chances are, they won’t take the code, or they’ll sulk if they do
In general, anything that causes universal (or near-universal) inclusion ofheader files can seriously weaken the reuse potential of your systems, andchanges to those universal files will cause rebuilds left, right and centre Ofcourse, we have heard this argument before
There’s no reason why we can’t apply this same global technique at thepackage local level, though If each package keeps track of its own memory,then we get the global tracking without the binding Interestingly, though, atthe local level memory management can often be so simple that the problemcan simply dissolve away
7.2.2 Referencing
So we’re writing (say) an air combat game, though the genre is largely tant for this discussion We create a type of object called a missile, of whichthere are two varieties: one that just flies in a path dictated by an axial thrustand gravity, and another that homes in on a specific target that the player orNPC has selected It’s the latter object we’re interested in Here’s a sketch of thecode sections we’re interested in:
unimpor-class Missile : public GameObject{
HomingMissile( /*stuff*/, GameObject * pTarget );
/*virtual*/ void Update( float dt );
// bada bing
private:
GameObject * m_pTarget;
};
Trang 12When the homing missile is created, we provide a target object that it should
head towards When we update the missile, it does some sums to work out
tra-jectories and then adjusts itself accordingly:
void HomingMissile::Update( float dt )
But we’re not concerned about the maths and physics at this moment The
problem we need to address is this: suppose another missile blows up our target,
which is deleted (and potentially new objects such as explosions and debris put
in their place)? But hold on, our homing missile still has a pointer to a now
non-existent object, so next time it is updated we can be sure that something
undesirable will happen
How do we solve this? It’s part of the general problem of how we referenceobjects safely One common method is to prevent the application from holding
pointers to allocated game objects at all Instead, all object transactions use
object handles, which cannot be deleted We then have some kind of object
management interface that accepts handles and returns the relevant data:
class ObjectManager
{
public:
ObjectHandle CreateObject( const char * pType );
void FreeObject( ObjectHandle hObject );
MATHS::lin_Vector3 GetPosition( ObjectHandle hObj );
private:
ObjectFactory * m_pFactory;
};
So what is an ObjectHandleexactly? In short, it’s anything you want it to be It’s
called an opaque type because as far as the outside world – the client code – is
con-cerned, it cannot see what the type does Only the ObjectManagerknows what to
do with handles – how to create them, validate them and dereference them
As an oversimplified example, consider using a 32-bit integer as the handletype:
Trang 13Within the object manager, we have a table of pointers to objects that the ger indexes into:
inte-// ObjectManager.cppnamespace
{const int MAX_OBJECTS = 200;
GameObject * s_Objects[ MAX_OBJECTS ];
int s_iNextFreeObject = 0;
}
Now whilst that’s not the whole story, suffice to say that many games use asystem such as this, so there is a lot to recommend it Unfortunately, it’s not asystem that I favour The main objection is that handles collapse type informa-tion We went to such pains to design an elegant object hierarchy, and nowwe’ve reduced everything to either a lowest-common-denominator interface or
a hugely bloated monolithic one, because you need to be able to do anythingwith a handle that you could do with a GameObjector any of its subclasses.
Handles do have their uses, however, and we’ll discuss those later To solve ourimmediate problem, though, we can invoke the flexible reference-counting mecha-nism we discussed in the previous section Recall that we used a base class toimplement reference counting, which had a virtual function OnUnreferenced()and that was called when the reference count hits zero This mechanism can be pig-gybacked to do exactly what we require:
Trang 14{
int MAX_SCENERY = 111;
mem_Pool<Scenery> s_SceneryPool( MAX_SCENERY );
// Creator called by ObjectFactory
GameObject * createScenery(){
return( s_SceneryPool.Allocate() );
}}
The only thing we have to watch out for is always to call Release()on pointers
to GameObjects rather than delete Protecting the destructor could enforce this
somewhat, but protected status is too easy to remove via inheritance to be
watertight, and making the destructor private would cause compiler errors since
we could never destroy the GameObjectclass from a subclass
There’s another little gotcha waiting to bite us when we allocate gameobjects from a pool Previously, we encountered problems because we held on to
a pointer to an object that was deleted The pointer becomes invalid;
consider-ably more likely than not, it will address data inside a new object or point at
fragile memory manager data Dereferencing it would be a disaster However,
when we allocate objects from a pool, it is now extremely likely – indeed, given
long enough, 100% certain – that our cached pointer will point to the start of a
new object that is actually in use Ouch! This is a subtle and annoying bug
To get around this one, we need to create a smart pointer class Also, ever we create an object, we need to give it a unique identifier The simplest way
when-is to use a 32-bit integer:
Trang 15int s_iIdGenerator = 0;
}
GameObject::GameObject(): // …
, m_iUniqueId( s_iIdGenerator++ ){
}
Our smart pointer class obtains the unique identifier when it is created It canthen tell if the referred object is valid by comparing its cached identifier withthe actual ID of the object (which is safe because being in a pool, the data arenever deleted as such during game execution):
// ObjectPointer.hppclass ObjectPointer{