Data Alignment, Part 2: Objects on The Heap and The Stack
March 31, 2009 Page 1 of 3
[Continuing his two-part article on data alignment, originally printed in Game Developer magazine, game programming veteran Llopis explains how to align objects on the heap, and use this effectively for game coding.]
Last month we looked at data alignment, how to align static variables, and how to allocate aligned memory on the heap through a simple custom allocator. It's a good start, but we need more than that to be able to use alignment effectively in a game.
This month, I'll wrap up this topic by looking at how to allocate any object on the heap with a given alignment and explore allocations on the stack.
Be warned that this is going to take some digging into the dark, dusty corners of the C++ language. So sit comfortably by the fireplace, grab your trusty copy of the C++ Standard, and let's get started.
Aligning Objects on the Heap
The main problem with aligned_malloc is that it simply allocates a block of data, and we often want to allocate full instances of classes or structs with a particular alignment.
In a pinch, we can turn to the underused and unloved placement new. Placement new works just like the regular new you're used to, but it takes an extra parameter with the memory address where the object will be instantiated. Placement new won't allocate the memory for you-it will just create an object of the right type where you tell it to and call its constructor.
Combining placement new and aligned_malloc, you could create an aligned waypoint object on the heap. See Listing 1 below.
Listing 1: Aligned Waypoint Object Created on the Heap
void* buffer = aligned_malloc(sizeof(Waypoint), 16);
Waypoint* waypoint = new (buffer) Waypoint();
It works, but that's some ugly code, not the kind of thing you want to see all over your codebase. Not only are we responsible for keeping around the aligned pointer so we can free it ourselves, but we have to call the object's destructor by hand.
A cleaner way to do it would be to create custom operator new and operator delete functions for the class. Those operators would be responsible for doing all the bookkeeping behind the scenes: hanging onto the pointer, calling the destructor, and freeing the memory with the correct function. If we implement this approach with our Waypoint class, Listing 1 would be reduced to:
Waypoint* waypoint = new (aligned_malloc(sizeof(Waypoint), 16))) Waypoint();
That code is better, but we can simplify it further. Instead of passing the pre-allocated memory, we can simply pass the desired alignment, and let the custom operator new call aligned_malloc.
To free memory correctly, the corresponding operator delete also needs to call aligned_free. The allocation code is now:
Waypoint* waypoint = new (16) Waypoint();
That's much cleaner!
As we saw last month, memory allocated with aligned_malloc needs to be freed with aligned_free. Attempting to use the wrong free function on a block of memory will result in a heap corruption (and possibly some really hard to track down bugs).
That means that operator delete needs to know how the object was allocated and decide which free function to call. Since operator delete can't easily access the contents of the object it's creating, we would have to allocate extra space and set some flags around the memory that was just allocated, indicating which function was used.
Alternatively, a simple solution is to overwrite the default operator new and call aligned_malloc from it as well, but without any alignment. That way all memory is allocated through aligned_malloc and operator delete can safely call aligned_free for all objects.
Since creating all those operators is just a bunch of busy work, we can wrap them in a macro called DYNAMIC_ALIGNMENT. The advantage of the macro over a base class implementing those functions is that we can use the macro in any class or structure without requiring any inheritance or changing the data layout in any way.
Since the classes we want to align are often fairly small, low-level ones, that's a particularly important consideration. (See the source code for the full implementation).
Page 1 of 3