Once you start stuffing data into structs, you may be surprised just how big even small ones can get. Something that looks like five or six bytes really might end up costing eight. The culprit lives within two conceptually similar ideas called alignment and padding. Let’s explore both with some examples and see where the compiler chooses to place our data.
Let's look at a short example.
#include <iostream>
// Define the members that belong to this type
struct S
{
char c{};
int i{};
};
int main()
{
std::cout << sizeof(S) << '\n';
}
On most systems, this will print 8. A char needs only one byte, and an int needs four, so you might think this should only take 1 + 4 = 5 bytes. However, the compiler instead has added three bytes of padding.
Every primitive type carries an alignment requirement. The alignment of a type is the value by which the address of an object of that type must be a multiple. char objects are allowed to start anywhere and have an alignment requirement of 1. int variables almost always require an address that is a multiple of 4, 8 on 64 bit machines, while a double would generally require a multiple of 8, also 8 on modern 64 bit machines. If the compiler had placed the int i right after c at address 1, it wouldn't have met its requirement, hence the compiler added three padding bytes, putting i at offset 4 within the struct.
#include <iostream>
int main()
{
std::cout << alignof(char) << ' '
// Query the alignment requirement of this type
<< alignof(int) << ' '
<< alignof(double) << '\n';
}
Usually, this would result in an output of 1 4 8. These are the sizes of these types on a typical machine, but more importantly, they relate to the address constraint: an object has to have an address that is a multiple of its alignment requirement.
#include <iostream>
struct Mixed
{
char a{};
short b{};
int c{};
char d{};
};
int main()
{
std::cout << sizeof(Mixed) << '\n';
}
This arrangement typically results in a struct that is 12 bytes. Let's see why. The first member a is placed at offset 0.
Member b has an alignment requirement of 2, so a padding byte is introduced and b is placed at offset 2.
The c member has an alignment requirement of 4, and as the offset after b is already 4, c can just start there. d follows and it fits neatly at offset 8. At this point, nine bytes have been allocated for the data, but a struct also has its own alignment requirement that equals the maximum of the alignments of its members. In this case, that max alignment is 4, and thus the size of the Mixed struct must be a multiple of 4. The closest multiple to 9 is 12, so three extra padding bytes are added.
These trailing padding bytes weren't just an arbitrary decision. Imagine creating an array of these Mixed structs: a Mixed array needs its entire elements, not just the first one, to align appropriately. If the compiler hadn't added those trailing bytes, the second Mixed in the array would start at address 0 + 9 = 9, which is not a multiple of 4 and would misalign the members within that second Mixed object.
The order of your members will have a significant impact on how much space they occupy, as evidenced by the two examples below:
#include <iostream>
struct BadOrder
{
char a{};
int b{};
char c{};
};
struct GoodOrder
{
int b{};
char a{};
char c{};
};
int main()
{
std::cout << sizeof(BadOrder) << ' '
// Query the size in bytes without evaluating the operand
<< sizeof(GoodOrder) << '\n';
}
BadOrder typically ends up 12 bytes, three bytes for padding after a, the four bytes of b, one byte for c, and then the trailing three bytes required to meet the whole struct’s alignment of 4. GoodOrder is only 8 bytes long. The int, which requires larger alignment, a multiple of 4 on many platforms, comes first, which imposes no padding after it.
The two chars then stack perfectly on top of each other in the subsequent two bytes.
Finally, three padding bytes are appended at the end to meet the total 8 byte requirement dictated by the largest member's alignment, the 4 byte int. Clearly, placing the largest members first results in the minimal amount of padding.
This principle scales to larger types like double. Because a double likely forces 8 byte alignment, having many smaller types before it can cause tremendous padding if you're not careful about order.
#include <iostream>
struct WithDouble
{
char c{};
double d{};
int i{};
};
int main()
{
std::cout << sizeof(WithDouble) << '\n';
}
This struct might very well result in 24 bytes because of seven bytes of padding after the c to get d onto an 8 byte boundary. Then after d you are at address 16, which already satisfies i's alignment, and there's four bytes left over until you hit an 8 byte boundary to fulfill WithDouble’s alignment requirement, which will also bring you to 24 bytes.
If you reorder this struct, placing d and i first, this is much more likely to be 16 bytes.
One subtlety to remember is that a struct's size is guaranteed to be a multiple of its alignment requirement. This is why even if the last member fits perfectly and is aligned, there can be trailing padding bytes that serve to align the entire structure itself for array placement. The compiler is essentially reserving that space, even though it will never contain your useful data, solely to allow the efficient, one instruction access of aligned members on most hardware platforms. If your members aren't aligned, it might take two, or on certain architectures might even cause a system crash.
When the exact size of the struct is of consequence, particularly if you’ll be storing a lot of them, the rule of thumb is simple: roughly order the members from largest alignment requirement to smallest.
0 comments
No comments yet. Be the first.