• No se han encontrado resultados

Discusión de resultados

In document FACULTAD DE CIENCIAS EMPRESARIALES (página 43-49)

IV. Discusión y Propuesta

4.1 Discusión de resultados

[email protected]

W

ith the increasing complexity of game programming, the minimum memory requirements for games have skyrocketed. Today's games must effectively deal with the vast amounts of resources required to support graphics, music, video, anima-tions, models, networking, and artificial intelligence. As the project grows, so does the likelihood of memory leaks, memory bounds violations, and allocating more memory than is required. This is where a memory manager comes into play. By creating a few simple memory management routines, we will be able to track all dynamically allo-cated memory and guide the program toward optimal memory usage.

Our goal is to ensure a reasonable memory footprint by reporting memory leaks, tracking the percentage of allocated memory that is actually used, and alerting die pro-grammer to bounds violations. We will also ensure that die interface to die memory manager is seamless, meaning that it does not require any explicit function calls or class declarations. We should be able to take diis code and effortlessly plug it into any other module by including die header file and have everything else fall into place. The disad-vantages of creating a memory manager include die overhead time required for die man-ager to allocate memory, deallocate memory, and interrogate die memory for statistical information. Thus, this is not an option that we would like to have enabled for the final build of our game. In order to avoid these pitfalls, we are going to only enable the mem-ory manager during debug builds, or if the symbol ACTIVATE_MEMORY_MANAGER is defined.

Getting Started

The heart of the memory manager centers on overloading the standard new and delete operators, as well as using #define to create a few macros that allow us to plug in our own routines. By overloading the memory allocation and deallocation routines, we will be able to replace the standard routines with our own memory-tracking module.

These routines will log the file and line number on which the allocation is being requested, as well as statistical information.

66

The first step is to create the overloaded new and delete operators. As mentioned earlier, we would like to log the file and line number requesting the memory alloca-tion. This information will become priceless when trying to resolve memory leaks, because we will be able to track the allocation to its roots. Here is what the actual overloaded operators will look like:

inline void*

operator new(size_t size, const char *file, int line);

inline void*

operator new[](size_t size, const char *file, int line);

inline void operator delete( void *address );

inline void operator delete[]( void *address );

It's important to note that both the standard and array versions of the new and delete operators need to be overloaded to ensure proper functionality. While these dec-larations don't look too complex, the problem that now lies before us is getting all of the routines that will use the memory manager to seamlessly pass the new operator the additional parameters. This is where the #define directive comes into play.

#define new new( FILE , LINE )

tfdefine delete setOwner(_FILE_,_LINE_) .false ? setOwner("",0) : delete

#define malloc(sz) AllocateMemory(_FILE_,_LINE_,sz,MM_MALLOC) tfdefine calloc(num,sz)

AllocateMemory(_FILE_1_LINE_,sz*num,MM_CALLOC)

#define realloc(ptr,sz) AllocateMemory( FILE , LINE , sz, MM_REALLOC, ptr )

tfdefine free(sz) deAllocateMemory( FILE , LINE , sz,

MM_FREE )

The #define new statement will replace all new calls with our variation of new that takes as parameters not only the requested size of the allocation, but also the file and line number for tracking purposes. Microsoft's Visual C++ compiler provides a set of predefined macros, which include our required __FILE_ and LINE__ symbols [MSDN]. The #define delete macro is a little different from the #define new macro. It is not possible to pass additional parameters to the overloaded delete operator without creating syntax problems. Instead, the setOwnerQ method records the file and line number for later use. Note that it is also important to create the macro as a condi-tional to avoid common problems associated with multiple-line macros [DaltonOl].

Finally, to be complete, we have also replaced the mallocQ, callocQ, reallocQ, and the freeO methods with our own memory allocation and deallocation routines.

The implementations for these functions are located on the accompanying CD.

t I The AllocateMemoryO and deAllocateMemoryO routines are solely responsible for all on m CD memory allocation and deallocation. They also log information pertaining to the desired allocation, and initialize or interrogate the memory, based on the desired

action. All this information will then be available to generate the desired statistics to analyze the memory requirements for any given program.

Memory Manager Logging

Now that we have provided the necessary framework for replacing the standard mem-ory allocation routines with our own, we are ready to begin logging. As stated in the beginning of this gem, we will concentrate on memory leaks, bounds violations, and the actual memory requirements. In order to log all of the required information, we must first choose a data structure to hold the information relevant to memory alloca-tions. For efficiency and speed, we will use a chained hash table. Each hash table entry will contain the following information:

struct MemoryNode {

size_t actualSize;

size_t reportedSize;

void *actualAddress;

void *reportedAddress;

char sourceFile[30];

unsigned short sourceLine;

unsigned short paddingSize;

char options;

long predefinedBody;

ALLOC_TYPE allocationType;

MemoryNode *next, *prev;

};

This structure contains the size of memory allocated not only for the user, but also for the padding applied to the beginning and ending of the allocated block. We also record the type of allocation to protect against allocation/deallocation mis-matches. For example, if the memory was allocated using the new[] operator and deal-located using the delete operator instead of the delete[] operator, a memory leak may occur due to object destructors not being called. Effort has also been taken to mini-mize the size of this structure while maintaining maximum flexibility. After all, we don't want to create a memory manager that uses more memory than the actual appli-cation being monitored.

At this point, we should have all of the information necessary to determine if there are any memory leaks in the program. By creating a MemoryNode within the AllocateMemoryO routine and inserting it into the hash table, we will create a history of all the allocated memory. Then, by removing the MemoryNode within the deAllo-cateMemoryO routine, we will ensure that the hash table only contains a current list-ing of allocated memory. If upon exitlist-ing the program there are any entries left within the hash table, a memory leak has occurred. At this point, the MemoryNode can be interrogated to report the details of the memory leak to the user. As mentioned previ-ously, within the deAllocateMemoryO routine we will also validate that the method

used to allocate the memory matches the deallocation method; if not, we will note the potential memory leak.

Next, let's gather information pertaining to bounds violations. Bounds violations occur when applications exceed the memory allocated to them. The most common place where this happens is within loops that access array information. For example, if we allocated an array of size 10, and we accessed array location 11, we would be exceed-ing the array bounds and overwritexceed-ing or accessexceed-ing information that does not belong to us. In order to protect against this problem, we are going to provide padding to the front and back of the memory allocated. Thus, if a routine requests 5 bytes, the AllocateMem-oryO routine will actually allocate 5 + sizeofllong)*2*paddmgSize bytes. Note that we are using longs for the padding because they are defined to be 32-bit integers. Next, we must initialize the padding to a predefined value, such as OxDEADCODE. Then, upon deal-location, if we examine the padding and find any value except for the predefined value, we know that a bounds violation has occurred. At this point, we would interrogate die corresponding MemoryNode and report die bounds violation to the user.

The only information remaining to be gathered is the actual memory require-ment for the program. We would like to know how much memory was allocated, how much of the allocated memory was actually used, and perhaps peak memory alloca-tion informaalloca-tion. In order to collect this informaalloca-tion we are going to need another container. Note that only the relevant members of the class are shown here.

class MemoryManager {

public:

unsigned int m_totalMemoryAllocations;

unsigned int m_totalMemoryAllocated; / / I n bytes unsigned int m_totalMemoryUsed; / / I n bytes unsigned int m_peakMemoryAllocation;

}|

Within the AllocateMemoryO routine, we will be able to update all of the Memo-ryManager information except for the m_totalMemory Used variable. In order to deter-mine how much of the allocated memory is actually used, we will need to perform a trick similar to the method used in determining bounds violations. By initializing the memory within the AllocateMemoryO routine to a predefined value and interrogating the memory upon deallocation, we should be able to get an idea of how much mem-ory was actually utilized. In order to achieve decent results, we are going to initialize the memory on 32-bit boundaries, once again, using longs. We will also use a prede-fined value such as OxBAADCODE for initialization. For all remaining bytes that do not fit within our 32-bit boundaries, we will initialize each byte to OxE or static_cast<char>(OxBAADCODE). While this method is potentially error prone because there is no predefined value to which we could initialize the memory and ensure uniqueness, initializing the memory on 32-bit boundaries will generate far bet-ter results than initializing on byte boundaries.

Reporting the Information

Now that we have all of the statistical information, let's address the issue of how

we should report it to the user. The implementation that is included on the CD records all information to a log file. Once the user has enabled the memory manager and run the program, upon termination a log file is generated containing a listing of all the memory leaks, bounds violations, and the final statistical report.

The only question remaining is: how do we know when the program is terminat-ing so that we can dump our log information? A simple solution would be to require the programmer to explicitly call the dumpLogReport() routine upon termination.

However, this goes against the requirement of creating a seamless interface. In order to determine when the program has terminated without the use of an explicit func-tion call, we are going to use a static class instance. The implementafunc-tion is as follows:

class Initialize

{ public: Initialize() { InitializeMemoryManager(); } };

static Initialize InitMemoryManager;

bool InitializeMemoryManager() {

static bool hasBeenlnitialized = false;

if (sjnanager) return true;

else if (hasBeenlnitialized) return false;

else {

s_manager->release(); // Releases the hash table and calls free( sjnanager ); // the dumpLogReport() method sjnanager = NULL;

}

The problem before us is to ensure that the memory manager is the first object to be created and the very last object to be deallocated. This can be difficult due to the order in which objects that are statically defined are handled. For example, if we cre-ated a static object that alloccre-ated dynamic memory within its constructor, before the memory manager object is allocated, the memory manager will not be available for memory tracking. Likewise, if we use the ::atexit() method to call a function that is responsible for releasing allocated memory, the memory manager object will be released before the ::atexit() method is called, thus resulting in bogus memory leaks.

In order to resolve these problems, the following enhancements need to be added.

First, by creating the InitMemoryManager object within the header file of the memory manager, it is guaranteed to be encountered before any static objects are declared.

This holds true as long as we #include that memory manager header before any static definitions. Microsoft states that static objects are allocated in the order in which they are encountered, and are deallocated in the reverse order [MSDN]. Second, to ensure that the memory manager is always available we are going to call the InitializeMemo-ryManager() routine every time within the AllocateMemoryO and DeallocateMemoryQ routines, guaranteeing that the memory manager is active. Finally, in order to ensure that the memory manager is the last object to be deallocated, we will use the ::atexit() method. The ::atexit() method works by calling the specified functions in the reverse order in which they are passed to the method [MSDN1]. Thus, the only restriction that must be placed on the memory manager is that it is the first method to call the ::atexit() function. Static objects can still use the ::atexit() method; they just need to make sure that the memory manager is present. If, for any reason, the InitializeMem-oryManagerQ function returns false, then this last condition has not been met and as a result, the error will be reported in the log file.

Given the previous restriction, there are a few things to be aware of when using Microsoft's Visual C++. The ::atexit() method is used extensively by internal VC++

procedures in order to clean up on shutdown. For example, the following code will cause an ::atexit() to be called, although we would have to check the disassembly to see it.

void Foo() { static std::string s; }

While this is not a problem if the memory manager is active before the declara-tion of s is encountered, it is worth noting. Despite this example being completely VC++ specific, other compilers might differ or contain additional methods that call ::atexit() behind the scenes. The key to the solution is to ensure that the memory manager is initialized first.

Things to Keep in Mind

Besides the additional memory and time required to perform memory tracking, there are a few other details to keep in mind. The first has to deal with syntax errors that can be encountered when #induding other files. In certain situations, it is possible to generate syntax errors due to other files redefining the new and delete operators. This is especially noticeable when using STL implementations. For example, if we #include

"MemoryManager.h"a.nd then #include <map>, we will generate all types of errors. To resolve this issue, we are going to be using two additional header files: new_on.h and new_off.h. These headers will simply #define and #undefine the new!'delete macros that were created earlier. The advantage of this method includes the flexibility that we achieve by not forcing the user to abide by a particular #include order, and avoids the complexity when dealing with precompiled headers.

tfinclude "new_off.h"

#include <map>

^include <string>

#include <A11 other headers overloading the new/delete operators>

#include "new_on.h"

^include "MemoryManager.h" // Contains the Memory Manager Module tfinclude "Custom header files"

Another issue we need to address is how to handle libraries that redefine the new and delete operators on their own. For example, MFC has its own system in place for handling the new and delete operators [MSDN2]. Thus, we would like to have MFC classes use their own memory manager, and have non-MFC shared game code use our memory manager. We can achieve this by inserting the #indude "new_off.h" header file right after the #//2&/'created by the ClassWizard.

#ifdef _DEBUG

^include "new_off.h" // Turn off our memory manager tfdefine new DEBUG_NEW

tfundef THIS_FILE

static char THIS_FILE[] = _FILE__;

#endif

This method will allow us to keep the advantages of MFC's memory manager, such as dumping CC%>rt-derived classes on memory leaks, and still provide the rest of the code with a memory manager.

Finally, keep in mind the requirements for properly implementing'the setOwnerQ method used by the delete operator. It is necessary to realize that the implementation is more complicated than just recording the file and line number; we must create a stack implementation. This is a result of the way that we implemented the delete macro. Take, for example, the following:

File 1: line 1: class B { B() {a = new int;} ~B() {delete a;} };

File 2: line 1: B *objectB = new B;

File 2: line 2: delete objects;

The order of function calls is as follows:

1. new( objects, File2, 1 2. new( a, Filel, 1

3. setOwner( File2, 2 );

4. setOwner( Filel, 1 );

5. delete( a );

6. delete( objects );

As should be evident from the preceding listing, by the time the delete operator is called to deallocate objectB, we will no longer have the file and line number informa-tion unless we use a stack implementainforma-tion. While the soluinforma-tion is straightforward, the problem is not immediately obvious.

Further Enhancements

, c , Within the implementation provided on the CD accompanying this book, there are on m CD several enhancements to the implementation discussed here. For example, there is the option for the user to set flags to perform more comprehensive memory tests.

Options also exist for setting breakpoints when memory is deallocated or reallocated so that the programs stack can be interrogated. These are but a few of the possibilities that are available. Other enhancements could easily be included, such as allowing a program to check if any given address is valid. When it comes to memory control, the options are unlimited.

References

[DaltonOl] Dalton, Peter, "Inline Functions versus Macros," Game Programming Gems II, Charles River Media. 2001.

[McConnell93] McConnell, Steve, Code Complete, Microsoft Press. 1993.

[MSDN1] Microsoft Developer Network Library, http://msdn.microsoft .com/

Iibrary/devprods/vs6/visualc/yclang/_pluslang_initializing_static_objects.htm [MSDN2] Microsoft Developer Network Library, http://msdn.microsoft

.com/library/devprods/vs6/visualc/vccore/core_memory_management_with_mf c.3a_.overview.htm

[Myers98] Myers, Scott, Effective C++, Second Edition, Addison-Wesley Longmont, Inc. 1998.

In document FACULTAD DE CIENCIAS EMPRESARIALES (página 43-49)

Documento similar