The Mathematics of Optimal Struct Field Ordering
If you’ve spent time writing Go (or C/C++), you’ve probably come across a common piece of advice: “Arrange struct fields from largest to smallest to save memory.”. Does this advice actually work? If so, why?
Its easy to test this advice with a simple example
package main
import (
"fmt"
"unsafe"
)
type InefficientOrder struct {
A bool // 1 byte
B int16 // 2 bytes
C int32 // 4 bytes
D int64 // 8 bytes
E bool // 1 byte
}
type EfficientOrder struct {
D int64 // 8 bytes
C int32 // 4 bytes
B int16 // 2 bytes
A bool // 1 byte
E bool // 1 byte
}
func main() {
fmt.Printf("Size of InefficientOrder struct: %d bytes\n", unsafe.Sizeof(InefficientOrder{}))
fmt.Printf("Size of EfficientOrder struct: %d bytes\n", unsafe.Sizeof(EfficientOrder{}))
}
When you run this code, you’ll see
Size of InefficientOrder struct: 24 bytes
Size of EfficientOrder struct: 16 bytes
A 33% saving in memory - just by reordering fields! So the advice definitely works in this case. But how can we be certain that placing fields in decreasing order of their sizes (actually, alignment requirements) is the optimal way to pack fields all the time?
We’ll take the help of Mathematics here. While there are several ways to approach this proof, the following less ‘pure’ solution is the one that clicked for me.
- Prove adjacent pair optimality: Show that swapping any two adjacent fields (where the smaller one comes first) reduces or never increases padding.
- Apply Adjacent Pair Optimisation Repeatedly : Show that repeatedly applying this swap leads to an optimally packed struct.
Step 1: Prove Adjacent Pair Optimality
We’re trying to prove that:
If any two adjacent fields and with alignment requirements and are out of order (), then swapping them reduces or at least does not increase the total padding
Proof
First, lets introduce some notations and definitions
-
is the arbitrary offset in memory from which the struct is ‘laid out’
-
is the alignment requirement of field . Typically this equals the size of the field, but for our proof we’ll consider them as potentially different.
-
(and ) represents the current offset in memory
-
is the function that returns the next offset value (including ) at which a field with alignment requirement can be placed.
Mathematically, this is . In other words, the smallest number greater than or equal to that is a multiple of
Now let’s consider the two possible orderings [A, B], [B, A] (remember, )
Case 1: [A, B]
Field is placed at
Field is placed after A, but since its offset needs to satisfy the alignment requirement , its actual offset is:
where
Note that
The total end offset is
Case 2: [B, A]
Field is now at
Field is placed after satisfying its alignment requirement , so it should be placed at:
where
Here
The total end offset is
Comparing the two orders
At this point, we can’t make a general conclusion about the equation. All we know is that and
However, we can leverage a key insight: alignment requirements are powers of 2. This implies that:
- If , then is actually a multiple of . Specifically, for some integer .
- An address aligned to is automatically aligned to as well. (Because if a number is divisible by 8, it’s also divisible by 4, 2, and 1.)
Using these insights:
-
:
Since is a multiple of , any address that is a multiple of is also a multiple of . This means is also a multiple of , but it might be farther from than is.
-
The worst-case padding in Case 1 is larger than worst-case padding in Case 2
The maximum possible value of is , which occurs when is just one byte past a multiple of . The maximum possible value of is , which occurs when is just one byte past a multiple of .
Now, for power-of-2 alignments where :
- The worst-case increase in starting position is
- The worst-case reduction in padding is
Since for all , we can generally expect:
which means
or equivalently, . In other words, swapping the two fields (when they are out of order) does not worsen—and typically improves—the overall packing.
Step 2: Apply Adjacent Pair Optimisation Repeatedly
Step 1 proves something powerful: swapping any adjacent pair of fields where a smaller alignment comes before a larger alignment will either improve memory usage or leave it unchanged. Never worse, always better or the same.
So what happens if we want to optimally pack all fields in a struct? We simply need to apply this adjacent pair optimization repeatedly across all fields. For each pair that’s out of order (smaller alignment before larger), we swap them.
If that repeated pair comparison and swapping reminds you of bubble sort - well, you are right.
Just as bubble sort guarantees a sorted array, our repeated swapping guarantees we’ll end up with struct fields arranged in strictly descending order of alignment requirements. And since each swap either reduces the memory footprint or keeps it the same (never increases it), this final arrangement must be optimal. This is why placing larger struct fields first is always the most memory-efficient ordering.
Remember, premature optimisation is evil - or in this case, can lead to less readable code, so apply this technique judiciously where memory efficiency genuinely matters for your application’s performance characteristics.