Memory management is a very important topic in game development. All games go through a period in development where memory is running low and the art team would like some more for extra textures or meshes. The way memory is laid out is also vitally important to the performance of your game. Understanding when to use stack memory, when to use heap memory, and the performance implications of each are important factors in to being able to optimize your programs for cache coherency and data locality. Before you can understand how to approach those problems you will need to understand the different places where C++ programs can store their data.
There are three places in C++ where you can store your memory: There is a static space for storing static variables, the stack for storing local variables and function parameters, and the heap (or free store) from where you can dynamically allocate memory for different purposes.
Static Memory
Static memory is handled by the compiler and there isnt much to say about it. When you build your program using the compiler, it sets aside a chunk of memory large enough to store all of the static and global variables defined in your program. This includes strings that are in your source code, which are included in an area of static memory known as a string table.
Theres not much else to say regarding static memory, so well move on to discussing the stack.
The C++ Stack Memory Model
The stack is more difficult to understand. Every time you call a function, the compiler generates code behind the scenes to allocate memory for the parameters and local variables for the function being called. Listing 1-1 shows some simple code that we then use to explain how the stack operates.
Listing 1-1. A Simple C++ Program
void function2(int variable1)
{
int variable2{ variable1 };
}
void function1(int variable)
{
function2(variable);
}
int _tmain(int argc, _TCHAR* argv[])
{
int variable{ 0 };
function1(variable);
return 0;
}
The program in Listing 1-1 is very simple: It begins with _tmain , which calls function1 which calls function2 . Figure illustrates what the stack would look like for the main function.
Figure 1-1.
The stack for tmain
The stack space for main is very simple. It has a single storage space for the local variable named variable . These stack spaces for individual functions are known as stack frames .When function1 is called, a new stack frame is created on top of the existing frame for _tmain . Figure shows this in action.
Figure 1-2.
The added stack frame for function1
When the compiler creates the code to push the stack frame for function1 onto the stack it also ensures that the parameter variable is initialized with the value stored in variable from _tmain . This is how parameters are passed by value. Finally, Figure shows the last stack frame for function2 added to the stack.
Figure 1-3.
The complete stack frame
The last stack frame is a little more complicated but you should be able to see how the literal value 0 in _tmain has been passed all the way along the stack until it is eventually used to initialize variable2 in function2 .
The remaining stack operations are relatively simple. When function2 returns the stack frame generated for that call is popped from the stack. This leaves us back at the state presented in Figure . Thats all you need to know to understand the basic functionality of a stack in C++.
Unfortunately things arent actually this simple. The stack in C++ is a very complicated thing to fully understand and requires a bit of assembly programming knowledge. That topic is outside the scope of a book aimed at beginners, but its well worth pursuing once you have a grasp of the basics. The article Programmers Disassemble in the September 2012 edition of Game Developer Magazine is an excellent introductory article on the operation of the x86 stack and well worth a read, available free from http://www.gdcvault.com/gdmag .
This chapter hasnt covered the ins and outs of how references and pointers are handled on the stack or how return values are implemented. Once you begin to think about this, you might begin to understand how complicated it can be. You might also be wondering why its useful to understand how the stack works. The answer lies in trying to work out why your game has crashed once it is in a live environment. Its relatively easy to work out why a game crashes while you are developing, as you can simply reproduce the crash in a debugger. On games that have launched, you might receive a file known as a crash dump, which does not have any debugging information and simply has the current state of the stack to go on. At that point you need to look out for the symbol files from the build that let you work out the memory addresses of the functions that have been called, and you can then manually work out which functions have been called from the addresses in the stack and also try to figure out which function passed along an invalid memory address of value on the stack.
This is complicated and time-consuming work, but it does come up every so often in professional game development. Services such as Crashlytics for iOS and Android or BugSentry for Windows PC programs can upload crash dumps and provide a call stack for you on a web service to help alleviate a lot of the pain from trying to manually work out what is going wrong with your game.
The next big topic in memory management in C++ is the heap.
Working with Heap Memory
Manually managing dynamically allocated memory is sometimes challenging, slower than using stack memory, and also very often unnecessary. Managing dynamic memory will become more important for you once you advance to writing games that load data from external files, as its often impossible to tell how much memory youll need at compile time. The very first game I worked on prevented programmers from allocating dynamic memory altogether. We worked around this by allocating arrays of objects and reusing memory in these arrays when we ran out. This is one way to avoid the performance cost of allocating memory.
Allocating memory is an expensive operation because it has to be done in a manner that prevents memory corruption where possible. This is especially true on modern multiprocessor CPU architectures where multiple CPUs could be trying to allocate the same memory at the same time. This chapter is not intended to be an exhaustive resource on the topic of memory allocation techniques for game development, but instead introduces the concept of managing heap memory.
Listing 1-2 shows a simple program using the new and delete operators.
Listing 1-2. Allocating Memory for a class Dynamically