Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 30 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
30
Dung lượng
208,51 KB
Nội dung
template<class T> T * mem_Pool<T>::Allocate() { T * pItem = m_FreeItems.top(); m_FreeItems.pop(); return( pItem ); } template<class T> void mem_Pool<T>::Free( T * pItem ) { // Some security checks required here, to // prevent multiple frees, frees of invalid data // and so on. m_FreeItems.push( pItem ); } template<class T> void mem_Pool<T>::FreeAll() { m_FreeItems.clear(); for( int i = 0; i < m_iNumItems; ++i ) { m_FreeItems.push( &m_Items[i] ); } } Notice that the pool class allocates an array of objects dynamically. This means that any class that requires pooling must provide a default constructor. Notice also that simply allocating from the pool returns an object that may have been used 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. }; Object-oriented game development286 8985 OOGD_C07.QXD 2/12/03 1:07 pm Page 286 namespace { 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 ); } void * Thing::operator new ( size_t ) { return( s_Pool.Allocate() ); } void Thing::operator delete( void * p ) { s_Pool.Free( reinterpret_cast<Thing *>(p) ); } Game objects 287 3You may also wish to add a static Initialise() function to the class to call pool<T>::FreeAll() at certain points within the code. 8985 OOGD_C07.QXD 2/12/03 1:07 pm Page 287 A really dull application for pools In the course of development, you’ll write some really fun code and you’ll write some 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 struc- ture where you can add and remove items in constant time, you’d be hard-pushed to find a better container class. The temptation to use STL’s std::list class is overwhelming, but a cursory investigation of any of the common implementations will yield the slightly disturbing realisation that a fair number of calls to operators new and delete take place during add and remove operations. Now, for some purposes that may not matter – if you do only one or two per game loop, big deal. But if you’re adding and removing lots of things from lists on a regular basis, then new and delete are going to hurt. So why are new and delete being called? If you require objects to be in sev- eral lists at once, then the link fields need to be independent of the object in a node class. And it’s the allocation and freeing of these nodes that can cause the invocation of dynamic memory routines. The thought occurs: why not write an allocator for nodes that uses our pools, and plug it into the STL list class? After all, they provide that nice little second 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 too critically on which particular version of STL you are using, so although your solution 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 con- 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. Asymmetric heap allocation Imagine 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, because it’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 bit more 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 the start of the level, allocate when required, and the only free we will ever need is the release of everything at the end of the level. Object-oriented game development288 8985 OOGD_C07.QXD 2/12/03 1:07 pm Page 288 This technique generalises to any object or family of objects whose exact maximum 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 pool allocator: // mem_AsymmetricHeap.hpp class mem_AsymmetricHeap { // This constructor creates a heap of the // requested size using new. 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; private: // Pointer to the managed block. char * m_pData; // Precomputed end of the heap. char * m_pEndOfData; // Where we get the next allocation from. char * m_pNextAllocation; // Size of the heap. int m_iHeapBytes; // Should the memory be freed on destruction? bool m_bDeleteOnDestruct }; Game objects 289 8985 OOGD_C07.QXD 2/12/03 1:07 pm Page 289 // mem_AsymmetricHeap.cpp #include "mem_AsymmetricHeap.hpp" mem_AsymmetricHeap::mem_AsymmetricHeap( int iHeapBytes ) : m_pData( new char [ iHeapBytes ] ) , m_pEndOfData( m_pData + iHeapBytes ) , m_pNextAllocation( m_pData ) , m_iHeapBytes( iHeapBytes ) , m_bDeleteOnDestruct( true ) { } mem_AsymmetricHeap:: mem_AsymmetricHeap( char * pMem, int iBytes ) : m_pData( pMem ) , m_pEndOfData( pMem + iBytes ) , m_pNextAllocation( pMem ) , m_iHeapBytes( iBytes ) , m_bDeleteOnDestruct( false ) { } mem_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; } Object-oriented game development290 8985 OOGD_C07.QXD 2/12/03 1:07 pm Page 290 The 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 in memory 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 moment longer. 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 are usually 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 who writes 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 Game objects 291 8985 OOGD_C07.QXD 2/12/03 1:07 pm Page 291 pointer leaks. So they write a string class, and this allows such semantic neat- ness as: String a = "hello"; String b = a; Now there are (at least) two schools of thought as to what that innocent- looking = should do. One says that the request is to copy a string, so let’s do just that. Schematically: strcpy( b.m_pCharPtr, a.m_pCharPtr ); However, the second school says that this is wasteful. If b never changes, why not just sneakily cache the pointer? b.m_pCharPtr = a.m_pCharPtr; But what happens if b changes? Well, we then need to copy a into b and update b. This scheme – called copy-on-write (COW) – is implemented by many string classes up and down the land. It opens up the can of worms (which is what COW really stands for) inside Pandora’s box, because the string class suddenly becomes a complex, reference-counting and rather top-heavy affair, rather than the high-performance little beast we hoped for. So if we’re to avoid COW, we are then forced into using strings that forcibly allocate on creation/copy/assignment, and free on destruction, including pass- ing out of scope. In other words, strings can generate a substantial number of hits to the memory management system(s), slowing performance and leading to fragmentation. 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 arrays and the C standard library interface. Unfortunately, this does reject some of the power that can be derived from a string class. For example, consider a hash table that maps names to objects: template<class Key,class Type> class hash_table { // Your implementation here. }; namespace { hash_table<char *,GameObject *> s_ObjectMap; } Object-oriented game development292 8985 OOGD_C07.QXD 2/12/03 1:07 pm Page 292 The 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 )); } private: const char * m_pString; }; namespace { hash_table<string_proxy,GameObject *> s_ObjectMap; } This works reasonably well, but beware of those string pointers! Consider the following code: namespace { string_proxy s_strText; } void foo( const char * pText ) { char cBuffer[256]; Game objects 293 8985 OOGD_C07.QXD 2/12/03 1:07 pm Page 293 strcpy( cBuffer, pText ); s_strText = string_proxy( cBuffer ); } void bar() { int x; // Some stuff involving x. } void main() { foo( "hello" ); bar(); if (s_strText == string_proxy( "hello" ) ) { printf( "yes!\n" ); } else { printf( "no!\n" ); } } This will – at least some of the time – print ‘no!’ and other times may print ‘yes!’ or perhaps just crash. Why? Because cBuffer is allocated on the stack, and the call 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’t slower 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 the maximum expected string length: class string { public: // A string interface here. private: enum { MAX_CAPACITY = 256; } char m_cBuffer[ MAX_CAPACITY ]; }; Object-oriented game development294 8985 OOGD_C07.QXD 2/12/03 1:07 pm Page 294 The 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. Starting with 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 allo- cate from the pool of the smallest suitable size that can contain the new length. 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 new and delete should 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 con- text of the priorities of the project, and they may do more harm than good. 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. void * operator new(size_t size, const char * pFile, int iLine); #else #define MEM_LogNew( Class )\ new Class #endif Game objects 295 8985 OOGD_C07.QXD 2/12/03 1:07 pm Page 295 [...]... }; // ObjectPointer.cpp #include "ObjectPointer.hpp" #include "GameObject.hpp" 8985 OOGD_C07.QXD 2/12/03 1:07 pm Page 301 Game objects ObjectPointer::ObjectPointer() : m_pObject( 0 ) , m_iObjectId( -1 ) { } ObjectPointer::ObjectPointer( GameObject * pObject ) : m_pObject( pObject ) , m_iObjectId( pObject->GetId() ) { } GameObject * ObjectPointer::operator*() { GameObject * pObject = 0; if ( m_iObjectId... the object you wish to refer to There must be only one reference stub per object instance, so we bury the whole reference and handle creation within the GameObject component: // GameObject.hpp #include typedef sys_Handle ObjectHandle; typedef sys_Reference ObjectReference; class GameObject { public: GameObject(); ~GameObject(); // stuff… ObjectHandle... simplest way is to use a 32-bit integer: // GameObject.hpp class GameObject { public: GameObject(); int GetId() const { return m_iUniqueId; } 299 8985 OOGD_C07.QXD 300 2/12/03 1:07 pm Page 300 Object- orientedgamedevelopment // blah private: int m_iUniqueId; }; // GameObject.cpp #include "GameObject.hpp" namespace { int s_iIdGenerator = 0; } GameObject::GameObject() : // … , m_iUniqueId( s_iIdGenerator++... FreeReference( ObjectReference * pRef ); static void Initialise(); // Call this to set up or clear object referencing private: ObjectReference * m_pReferenceStub; }; // GameObject.cpp #include "GameObject.hpp" 307 8985 OOGD_C07.QXD 308 2/12/03 1:07 pm Page 308 Object- orientedgamedevelopment namespace { const int MAX_OBJECTS = 1000; mem_Pool s_ReferencePool( MAX_OBJECTS ); } GameObject::GameObject()... invalid objects in any further processing: void Game: :Update( float dt ) { for( GameObject * pObject = FirstObject(); pObject != 0; pObject = NextObject() ); { if ( pObject->IsValid() ) { pObject->Update( dt ); } } } So, when is a safe time to physically remove the invalid objects? At the end of a game loop is as good a choice as you will get: void Game: :Update( float dt ) { GameObject * pObject; for( pObject... pObject; for( pObject = FirstObject(); pObject != 0; pObject = NextObject() ); 8985 OOGD_C07.QXD 2/12/03 1:07 pm Page 311 Game objects { if ( pObject->IsValid() ) { pObject->Update( dt ); } } // More stuff to update // Remove invalid objects for( pObject = FirstObject(); pObject != 0; pObject = NextObject() ); { if ( !pObject->IsValid() ) { RemoveObject( pObject ); delete pObject; } } } Referencing failures... referred object is valid by comparing its cached identifier with the actual ID of the object (which is safe because being in a pool, the data are never deleted as such during game execution): // ObjectPointer.hpp class ObjectPointer { public: ObjectPointer(); ObjectPointer( GameObject * pObject ); GameObject * operator*(); // Const versions of this, too private: GameObject * m_pObject; int m_iObjectId;... be needed later on within the same game loop For example, consider a collision between a plane and a (non-homing) missile: // Somewhere deep in the collision system GameObject * pObject1 = //…; GameObject2 *pObject2 = //…; coll_Info aCollInfo; if ( Collided( pObject1, pObject2, &aCollInfo ) ) { pObject1->OnCollision( pObject2, &aCollInfo ); pObject2->OnCollision( pObject1, &aCollInfo ); } // Plane.cpp... 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 ObjectHandle... pObject = 0; if ( m_iObjectId == m_pObject->GetId() ) { // The object is valid pObject = m_pObject; } return( pObject ); } It should be stressed that this technique works only for pool objects, because of the call to GameObject::GetId() within operator*() If the object has been heap-allocated and subsequently deleted, and we make that call, then it’s Game Over before the game s over So how can we make it . ) { } ObjectPointer::ObjectPointer( GameObject * pObject ) : m_pObject( pObject ) , m_iObjectId( pObject->GetId() ) { } GameObject * ObjectPointer::operator*() { GameObject * pObject = 0; if ( m_iObjectId == m_pObject->GetId(). "GameObject.hpp" Object- oriented game development3 00 8985 OOGD_C07.QXD 2/12/03 1:07 pm Page 300 ObjectPointer::ObjectPointer() : m_pObject( 0 ) , m_iObjectId( -1 ) { } ObjectPointer::ObjectPointer(. ObjectPointer { public: ObjectPointer(); ObjectPointer( GameObject * pObject ); GameObject * operator*(); // Const versions of this, too. private: GameObject * m_pObject; int m_iObjectId; }; // ObjectPointer.cpp #include "ObjectPointer.hpp" #include