Grokking Advanced Go
Vote

0% completed

​

Struct Layout and Alignment

Size and Alignment

Padding

The Order Struct

A Zero-Size Field at the End

When Layout Matters

32-Bit Platforms

Key Takeaways

These two structs have the same five fields with the same types, and only the order differs. Before you read further, calculate how many bytes each one needs on amd64.

type OrderStatusA struct { Paid bool ID int64 Shipped bool Quantity int32 Refunded bool } type OrderStatusB struct { ID int64 Quantity int32 Paid bool Shipped bool Refunded bool }

Adding the sizes of the fields gives 8 + 4 + 1 + 1 + 1, which is 15 bytes. The first struct actually occupies 32 bytes and the second occupies 16. One million values of the first type therefore need twice the memory of one million values of the second.

The extra bytes exist because Go places each field according to an alignment rule. Which rule produces those bytes, and when is the order of fields worth changing?

Size and Alignment

Every type has a size, which is how many bytes it occupies. It also has an alignment, which is a number that its address must be a multiple of. An int64 on amd64 has an alignment of 8, so it can start at address 0, 8, or 16, but not at address 3. Hardware reads a value most efficiently when it starts on such a boundary, and some operations require it.

Three functions in the unsafe package report these numbers, and all three are evaluated when the program is compiled. unsafe.Sizeof gives the size, unsafe.Alignof gives the alignment, and unsafe.Offsetof gives the position of a field from the start of its struct. The Go specification guarantees the sizes of numeric types such as int32 and int64, while the sizes of int and of pointers depend on the platform.

Padding

To satisfy alignment, the compiler inserts unused bytes between fields, which are called padding. It also adds padding at the end of a struct, so that the size is a multiple of the struct's alignment. Without that rule, the second element of an array of the struct would start at an address that breaks its alignment. A struct's alignment is the largest alignment of any of its fields, and the Go specification guarantees that as well.

The Go specification does not promise a memory order for fields, but the compiler in Go 1.27 keeps them in the order you declare them. Field order is therefore the part of the layout that you control.

Go
Go

. . . .

Output:

A size 32 alignment 8 A offsets 0 8 16 20 24 B size 16 alignment 8 B offsets 0 8 12 13 14

The offsets of the first struct show each gap. Paid is at byte 0, but ID must start on a multiple of 8, so bytes 1 through 7 are padding. Shipped is at byte 16, and Quantity must start on a multiple of 4, so bytes 17 through 19 are padding. Refunded is at byte 24, and the size must be a multiple of 8, so bytes 25 through 31 are padding as well.

Field in OrderStatusASizeOffsetPadding after
Paid bool107
ID int64880
Shipped bool1163
Quantity int324200
Refunded bool1247
Total1517, giving 32 bytes
Field in OrderStatusBSizeOffsetPadding after
ID int64800
Quantity int32480
Paid bool1120
Shipped bool1130
Refunded bool1141
Total151, giving 16 bytes
The same fields in two orders
The same fields in two orders

The usual repair is to order fields from largest alignment to smallest. A field needs no padding before it when every earlier field has the same alignment or a larger one. All the padding then collects at the end.

The Order Struct

The Order type from this course already has a good layout. Its fields are 8, 16, and 24 bytes with an alignment of 8, followed by one bool.

FieldSizeOffsetPadding after
ID int64800
Customer string1680
Items []Item24240
Paid bool1487
Total497, giving 56 bytes

Moving Paid to the front does not change the size, because the 7 bytes of padding simply move with it. A second bool, however, shows the effect of field placement.

Go
Go

. . . .

Output:

flags split: 64 flags together: 56

The struct with both flags together still fits in 56 bytes, because Shipped uses a byte that was padding. Placing the flags apart adds a full 8 bytes.

A Zero-Size Field at the End

A field of type struct{} occupies no bytes, and it usually adds nothing to a struct. There is one exception. A zero-size field at the end of a non-empty struct makes the compiler add padding (CalcStructSize in cmd/compile/internal/types/size.go). Taking the address of that field must not give an address outside the struct, since that address could belong to the next object in memory.

Go
Go

. . . .

Output:

zero-size field first: 8 zero-size field last: 16

When Layout Matters

Field order matters when there are many values. One million OrderStatusA values in a slice need 32 MB, while one million OrderStatusB values need 16 MB. That saving comes with a second benefit. The CPU reads memory in blocks called cache lines, which are 64 bytes on amd64 (CacheLinePadSize in internal/cpu/cpu_x86.go). A smaller struct puts more values in each line, so a scan over the slice loads fewer lines.

Field order does not matter for a struct with a few instances, such as a configuration value or a server type. In those types, keep related fields together, since the grouping makes the code easier to read and the saved bytes make no measurable difference. Reorder a struct only when a measurement or a simple count of values shows that it is worth doing.

SituationReorder the fields?
Millions of values in a slice or mapYes
A struct allocated once per requestOnly if a measurement shows a benefit
A configuration or server structNo, keep related fields together
A struct whose fields are already 8 bytes eachNo, there is no padding to remove

You do not have to find padding by hand. The fieldalignment analyzer (a tool that checks source code for one kind of problem) from the golang.org/x/tools module reports structs that would use less memory with their fields sorted. It is a separate tool, and go vet does not run it.

32-Bit Platforms

Layout depends on the platform. On 32-bit x86 (GOARCH=386), a pointer is 4 bytes and an int64 has an alignment of only 4. The first program on this page therefore reports different numbers, and OrderStatusA shrinks from 32 bytes to 24.

GOARCH=386 go run main.go
A size 24 alignment 4 A offsets 0 4 12 16 20 B size 16 alignment 4 B offsets 0 8 12 13 14

This difference matters for the sync/atomic package. Its documentation states that on ARM, 386, and 32-bit MIPS, the caller must arrange for 64-bit alignment of values used with functions such as atomic.AddInt64. The atomic.Int64 type is aligned automatically.

Key Takeaways

  • Each type has a size and an alignment, reported by unsafe.Sizeof, unsafe.Alignof, and unsafe.Offsetof.
  • The compiler adds padding between fields and at the end of a struct, so the order of fields changes the size.
  • Ordering fields from largest alignment to smallest removes most padding.
  • A zero-size field at the end of a non-empty struct adds padding, while one at the start does not.
  • Layout matters when there are many values; for a few values, group fields by meaning.
  • Sizes and alignments depend on the platform, so never hard-code them.

Struct layout follows one rule: each field starts on a multiple of its alignment, and any gap is padding. Knowing that rule lets you read a struct's real size from its declaration. You can then shrink it when a count of values shows that the saving is real. For most structs, the right order is the one that is easiest to read.

Reading Progress

0%


Vote for new content

On This Page

Size and Alignment

Padding

The Order Struct

A Zero-Size Field at the End

When Layout Matters

32-Bit Platforms

Key Takeaways