Introduction
When learning Go you’ll find that a struct is essentially a custom data layout, and the “order” of its fields directly affects the actual size it occupies in memory and even the program’s performance.
Why is memory padding needed?
In theory a CPU can read data from any address, but in practice to improve hardware access efficiency the CPU usually reads memory in word-size units instead of single bytes. Generally:
- On 32-bit systems, the word size is 4 bytes
- On 64-bit systems, the word size is 8 bytes
Assume on a 64-bit system an int64 (8 Bytes) variable happens to cross two word boundaries, the CPU would need to perform two read operations and stitch the results together to obtain the full value. This not only adds extra work, on some hardware architectures it can even cause an exception.
char data[10]; // char is only 1 byte, so it can be placed anywhereint *p = (int *)&data[1]; // Assume &data[1] is 0x1001 (odd address)int val = *p; // On strictly aligned architectures (such as ARM, SPARC), there is a chance of directly triggering SIGBUS crashing.0x1000 0x1001 0x1002 0x1003 0x1004 [---------- int ----------]To avoid this, the compiler automatically ensures that the starting addresses of data meet certain rules — this is “memory alignment”. To achieve alignment the compiler inserts blank bytes between data, a process called “memory padding”.
Designing good memory layout
type BadStruct struct { A int8 // 1 Byte B int64 // 8 Bytes C int16 // 2 Bytes}
func main() { var bad BadStruct fmt.Printf("BadStruct Size: %d Bytes\n", unsafe.Sizeof(bad)) // 24 Bytes}In the bad example the int64 must start at an address that’s a multiple of 8, so the compiler inserts 7 bytes of padding. Since 1+7+8+2 = 18 is not a multiple of 8 there will be another 6 bytes of padding at the end, making the total size 24 bytes.
type GoodStruct struct { B int64 // 8 Bytes C int16 // 2 Bytes A int8 // 1 Byte}
func main() { var good GoodStruct fmt.Printf("GoodStruct Size: %d Bytes\n", unsafe.Sizeof(good)) // 16 Bytes}In the good example the total field size is only 11 bytes, but the struct itself must satisfy the largest alignment requirement (8 bytes for the int64 in this case), so the end is padded by 5 bytes, giving a final size of 16 bytes.
By simply reordering fields the example saves 24 - 16 = 8 bytes (almost 33%) of memory. If that struct is instantiated repeatedly, the savings can be substantial.
Empty structs and padding
type StructWithEmpty struct { A int32 B struct{} // Put on the End}If an empty struct is placed as the last field of another struct it will occupy memory to avoid creating pointers that go out of bounds; the Go compiler will pad an empty struct that appears as the final field.
Detecting memory alignment
go vet -fieldalignmentfieldalignment -fix ./...Summary
Understanding memory alignment not only “saves space” but also helps build better cache locality.
- Group fields of similar or nearby sizes together, typically ordering fields by size from largest to smallest (or vice versa).
- Do not place empty structs as the last field.
- Memory alignment is important but it is just one aspect of development — avoid over-engineering.
Further reading
- What does it mean by word size in computer? - Stack Overflow
- WHY IS THE STACK SO FAST? - Core Dumped