Fun with Memory Allocation
Just a bit of a fun puzzle today. Take a moment to look at the following two classes. Given an architecture where an int is four bytes and a character is one, what is the size of an instance of each class?
struct Alpha
{
int x;
char ch1;
int y;
char ch2;
} a1;
struct Beta
{
int x;
int y;
char ch_name[2];
} b2;
The most obvious answer would be 4+1+4+1, giving us 10 bytes for the first class, and 4+4+1+1 giving us another 10 bytes with our second class. Curiously though, this isn't actually the case. If we std::cout « sizeof(c1) « “, " « sizeof(c2);, we'll find a more interesting allocation: 16, 12.
So what gives? Since when did 4 + 4 + 1 + 1 start equaling numbers other than 10? Well, it doesn't. Arithmetic still works, so we don't have to worry about an premature start for the apocalypse. So what's the problem? There isn't one really, just a difference of expectations. Whereas we might expect the compiler's job to simply be to transcribe our higher level commands into assembly, the compiler itself has a slightly more complex view on the world. In addition to actually creating our structures, it makes sure that they fit together in a way that makes sense to the processor. In this case, it is providing both data structure alignment and padding.
If we borrow a definition from Wikipedia, we get that “Data alignment means putting the data at a memory offset equal to some multiple of the word size, which increases the system's performance due to the way the CPU handles memory. To align the data, it may be necessary to insert some meaningless bytes between the end of the last data structure and the start of the next, which is data structure padding." 1 In fact, if we read a little bit further, we find that if data structures aren't aligned properly, the computer might not actually read our data at all.
Thus, the below chart shows the actual alignment of our classes, Alpha and Beta, respectively. We can see that the ints got aligned so that they begin on memory locations divisible by the word size (4 bytes) of the system. Additionally, we can see that the data structure itself got padded out to the next multiple of the word size: 13 pads to 16, and 10 pads to 12.
![Memory alignment for Alpha and Beta]({{ post.url }}/assets/2014/04/memory-0.jpg)
At this point, we might decide that the actual size of the data structure is “the size of member variables padded to a four byte boundary, and the structure padded to a four byte boundary.”
Now we have more of a problem than we did. We're doing a much more complicated process to calculate the size of our structure, and we're still doing it wrong. Let's look back at a very interesting phrase in the original definition: “some multiple of the word size”. The word size on our system is four, so the compiler chose (4 * 1) as the multiple of four. Though it is the most efficient choice, it is still just an implementation detail. FooBarBaz Co's C++ compiler could quite legally choose to use a padding of 1092 and it would still be valid. Wasteful, yes, but also valid. Even then, that doesn't consider the possibility that the target architecture could use a word size other than four.
From this, we get the answer to our puzzle, and an important lesson, to boot. The only reliable way to find the size of our instance is sizeof(). Anything past that is just the side effect of an implementation detail. Taking that a step further, when we choose an abstraction, we give up the right to make any assumptions about the process being abstracted away.
So, returning to the original question, the only correct answer is that the size of a given class is the sizeof() the class.
Footnotes
The quote original appeared here http://en.wikipedia.org/wiki/Data_structure_alignment ↩︎