Padding and Packing
The layout of structures is defined by the ABI (Application Binary Interface). The typical mindset of an ABI designer is mechanical sympathy, with an eye towards performance (speed).
To talk about padding, we need to talk about alignment, size, and stride.
First is alignment. Native types tend to have an alignment, that is the architecture expects that a double
is either 4 or 8 bytes aligned (depending). This alignment then bleeds into compound types: the only way to ensure that a double
field is 8 bytes aligned is for the compound type containing the field to itself by 8 bytes aligned, hence a compound type's alignment is the greater alignment of any of its components.
Second is size. Native types have a size, too. For example a double
is typically 8 bytes. The size of the compound type will depend on the size and alignment of its components, as per the ABI rules.
Finally is stride. The stride of a type is the spacing between consecutive array elements. Swift is one of the few languages differentiating size and stride, in C the stride is always the size.
With that in mind, here is a typical product type ABI, as can generally be found in C:
- The alignment of the product type is the maximum alignment of any of its fields.
- The first field starts at the first byte of its product type.
- Any subsequent field starts at the next offset which guarantees its alignment, which may lead to "in-between" padding bytes.
- The size of the struct is rounded-up to a multiple of its alignment, which may lead to "tail" padding bytes.
And since a picture is clearer than words:
struct ProductType {
int i;
char __padding_0[P0];
double d;
char __padding_1[P1];
char c;
char __padding_2[P2];
};
On x64, we have:
int
: 4 bytes, 4-bytes aligned.
double
: 8 bytes, 8-bytes aligned.
char
: 1 byte, 1-byte aligned.
Therefore:
- P0 = 4. This is because
d
must start at an offset that is a multiple of 8, and after i
the offset is only 4.
- P1 = 0. This is because
c
must start at an offset that is a multiple of 1, and any offset is.
- P2 = 7. This is because the alignment of
ProductType
is 8, hence its size must be a multiple of 8.
Note that Swift would have P2 = 0.
The effect of packing is, very simply, to remove all padding.1
1 I am aware that some compilers take the opportunity to also allow specifying the struct alignment when packing. That's a potentially useful extra feature, but it's not packing.
Implications of Packing
The first and foremost implication is that you should implement it properly, or users will curse you.
For example, it seems reasonable to allow taking a reference (or pointer) to a field of a packed struct. However, this reference (or pointer) may now be under-aligned: that is, its alignment may be strictly less than the expected alignment of a pointer of this type.
Different languages & toolchains handle the situation differently:
- In Zig, pointer alignment is a part of the pointer type, so a compilation error will follow if one attempts to use an under-aligned pointer where a regular pointer is expected.
- In GCC (C or C++), the compiler handles under-aligned pointers properly within the function in which the pointer was created, but allows passing the under-aligned pointers to functions expecting a regular pointer... which leads to Undefined Behavior.
- In Rust, manipulating packed fields is
unsafe
, and the user is on the hook to tread carefully, or they will trigger Undefined Behavior.
I would argue that the situation of being able to create an under-aligned pointer, but then have to walk on eggshells forever, is not desirable from a user point of view, and thus that language designers going the packed
way should either disallow forming such pointers, or follow through and ensure they provide users the tooling they need to manipulate under-aligned pointers.
Alternatives
There are alternatives to packing, for the memory conscious.
As mentioned, packing is about avoiding padding, and there are two sources of padding:
- In-between fields padding.
- Tail padding.
Tail padding is eliminated in a language such as Swift which differentiates size & stride.
In-between fields padding is eliminated in a language such as Rust which reserves the right to re-order fields arbitrarily -- and typically will reorder them by descending alignment, which removes all in-between fields padding.
Those two alternatives, combined, remove nearly2 any and all padding in a struct
3 without introducing under-aligned references and pointers into the fray.
I would typically advise considering them first:
- Provide the same memory gains as packed, without taxing the user.
- Without performance penalty incurred from under-aligned pointers.
- Without headaches induced by under-aligned pointers.
You may still want to later introduce under-aligned pointers in the language, separately.
2 Unless reordering can interleave the fields from different field structs, there may still be some padding in between structs. For example, a struct A (int, char) followed by itself would still have 3 bytes of padding between the two instances, no matter the order.
3 Padding may still occur between array elements, the exact amount of padding inserted being equal to stride - size
.