agentsclimarketplace

Go perf struct layout

Skill ctoth/golang-skills-plugin/plugins/performant-golang/skills/go-perf-struct-layout

Agent skill plugins for Go code quality and performance work.

Install
npx -y skills add ctoth/golang-skills-plugin --skill go-perf-struct-layout

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Guides Go struct layout for performance — field alignment and padding, how field ORDER changes its size, ordering fields largest-to-smallest to shrink it (measured unsafe.Sizeof deltas), the fieldalignment analyzer and -fix, unsafe.Sizeof/Alignof/Offsetof, and why pointer-free structs scan faster under GC. Fires on "shrink this struct", "struct padding", "fieldalignment", "why is this struct so big", "align struct fields". Routes GC-scan to go-perf-allocator-internals, false sharing to go-perf-false-sharing.

SKILL.md

12.7 KB, as published. Nobody here has run it

Go Struct Layout

"The compiler adds padding … to bring its size up to a multiple of the largest element in the structure … if you are looking to optimise the memory usage of your program, you need to be looking closely at the definitions of all your data structures." — Dave Cheney, Padding is hard

Struct correctness and idiom (field naming, embedding, tags, zero-value design) belong to go-naming-and-style and go-zero-values-and-construction — read those for "how should this struct look." This skill owns one narrow performance fact: the order you write fields changes how many bytes the struct occupies and how much the GC must scan, and you can shrink a hot struct by reordering alone — same fields, same behavior, fewer bytes.

Whether you should reorder is a go-perf-methodology question (§5); the numbers below are real, measured under go1.26.0.

All Go below builds clean under gofmt, go vet, and go test on Go 1.26.


1. The Alignment Rules (the whole model)

Two guarantees from the language spec produce every padding byte you will ever see.

Sizes are fixed. For the numeric types the spec guarantees: byte, uint8, int8 = 1; uint16, int16 = 2; uint32, int32, float32 = 4; uint64, int64, float64, complex64 = 8; complex128 = 16 (Go spec, Size and alignment guarantees). On a 64-bit target a pointer, int, uintptr, map, and chan are 8; a string is 16 (ptr+len); a slice is 24 (ptr+len+cap).

Alignment is the rule that forces padding. The spec's minimal guarantees (Go spec):

  1. For a variable x of any type: unsafe.Alignof(x) is at least 1.
  2. For a variable x of struct type: unsafe.Alignof(x) is the largest of all the values unsafe.Alignof(x.f) for each field f of x, but at least 1.
  3. For a variable x of array type: unsafe.Alignof(x) is the same as the alignment of a variable of the array's element type.

From these, two consequences drive everything:

  • A field of alignment a must start at an offset that is a multiple of a. A int64 (align 8) cannot start at offset 1; the compiler inserts padding bytes before it. Measured: Alignof(bool)=1, Alignof(int32)=4, Alignof(int64)=8, Alignof(float64)=8, pointer align =8.
  • The struct's size rounds up to a multiple of the struct's own alignment (rule 2: the max field alignment) — so an array of the struct keeps every element aligned. That is the trailing padding (Padding is hard).

One layout fact has a correctness edge: on 32-bit platforms a 64-bit field accessed with sync/atomic must be 64-bit aligned, and the spec only guarantees alignment for "the first word in … an allocated struct, array, or slice" — so a misordered int64/uint64 atomic field can panic on 32-bit. Put such fields first, or use the typed atomic.Int64 (which carries its own alignment). The correctness rule is go-sync-primitives' / go-perf-atomics-vs-locks' to own; layout is just the lever.


2. Field Order Changes the Size — Measured

Same fields, two orders. Three bools interleaved with two int64s:

type Bad struct { // 19 bytes of data, but...
	a bool  // offset 0
	b int64 // offset 8  (7 pad bytes after a — int64 must land on a multiple of 8)
	c bool  // offset 16
	d int64 // offset 24 (7 pad bytes after c)
	e bool  // offset 32 (7 trailing pad bytes → size rounds to 40)
}

type Good struct { // identical fields, largest-alignment first
	b int64 // offset 0
	d int64 // offset 8
	a bool  // offset 16
	c bool  // offset 17
	e bool  // offset 18 (5 trailing pad bytes → size rounds to 24)
}

Measured with unsafe.Sizeof under go1.26.0:

Structunsafe.SizeofWasted paddingField offsets
Bad4021 bytesa=0, b=8, c=16, d=24, e=32
Good245 bytesb=0, d=8, a=16, c=17, e=18

The same layout drawn byte-by-byte makes the holes literal (. = padding):

Bad  (40):  a . . . . . . . | b b b b b b b b | c . . . . . . . | d d d d d d d d | e . . . . . . .
            0               8                 16                24                32
Good (24):  b b b b b b b b | d d d d d d d d | a c e . . . . .
            0               8                 16

A 40% reduction (40 → 24) from reordering alone — no field added or removed. A second, smaller case: struct{ a bool; b int32; c bool } measures 12 bytes, but struct{ b int32; a bool; c bool } measures 8 — a 33% cut. The rule of thumb: order fields from largest alignment to smallest and the holes collapse. (Exception: when a field must be cache-line isolated to avoid false sharing → §6.)


3. The fieldalignment Analyzer (and -fix)

Don't compute padding by hand — the analyzer does it and rewrites the struct. It "finds structs that would use less memory if their fields were sorted … and provides a suggested edit with the most compact order" (fieldalignment). It ships outside vet/gopls because its findings "very rarely indicate a significant problem," so install and run it standalone (fieldalignment):

go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest
fieldalignment ./...        # report only
fieldalignment -fix ./...   # rewrite structs to the compact order

Run against the Bad struct above, the real output was:

s.go:3:10: struct of size 40 could be 24

and -fix rewrote it to b int64; d int64; a bool; c bool; e bool — exactly largest-first, matching Good at 24 bytes. Always re-run your tests after -fix: reordering is safe for in-memory size but changes binary field offsets, so it can break code that depends on layout (unsafe, cgo, encoding/binary on the struct, reflection that assumes an order).


4. unsafe.Sizeof / Alignof / Offsetof — Prove It, Don't Guess

Never assert a struct's size from memory; measure it. These are compile-time constants (for fixed-size types), so they cost nothing at runtime (unsafe):

  • unsafe.Sizeof(x) — "the size in bytes of a hypothetical variable v … For a struct, the size includes any padding introduced by field alignment." It does not include memory referenced through the value — Sizeof of a slice is the 24-byte descriptor, not the backing array (unsafe).
  • unsafe.Alignof(x) — "the required alignment" (rule 2 above); for a struct field s.f it returns the field's in-struct alignment (unsafe).
  • unsafe.Offsetof(s.f) — "the number of bytes between the start of the struct and the start of the field" — the direct way to see the padding holes (unsafe).
fmt.Println(unsafe.Sizeof(Bad{}))         // 40
fmt.Println(unsafe.Offsetof(Bad{}.b))     // 8  — 7-byte hole after the leading bool
fmt.Println(unsafe.Sizeof([]byte(nil)))   // 24 — slice header only, not the data

A growing offset gap between adjacent fields is the padding; that is the cheapest diagnostic.


5. When Smaller Structs Actually Pay (and when not)

Shrinking a struct helps three ways, all proportional to how hot and numerous it is:

  • Cache footprint. A 64-byte cache line holds one 40-byte struct but two 24-byte ones. Iterating a []T of the smaller struct touches roughly half the cache lines, so the prefetcher and L1/L2 do less work — the win shows up on large slices, not on a single value.
  • Fewer allocator bytes. A smaller struct may drop into a smaller size class, so each heap allocation costs less and the heap grows slower (size-class mechanics → go-perf-allocator-internals).
  • Less GC scan work when the shrink also removes pointers (§6).

The tradeoff, explicitly: reordering trades the readable grouping of fields (logical clusters, related fields together) for compactness. For a struct that is rare, short-lived, or not on a hot path, the padding is irrelevant and you should keep the order that reads best — go-perf-methodology's "is this worth optimizing?" gate applies. Reorder for size only when a profile or an allocation count says this struct's footprint matters; otherwise the clarity loss is unpaid-for. Don't sprinkle fieldalignment -fix across a whole codebase.


6. Pointer-Free Structs Scan Faster Under GC

Beyond raw size, the GC has to scan a struct for pointers — and field layout decides how much. fieldalignment reports a second diagnostic, "pointer bytes": "how many bytes of the object the garbage collector has to potentially scan for pointers." Its own examples: struct { uint32; string } "have 16 pointer bytes because the garbage collector has to scan up through the string's inner pointer," while struct { string; uint32 } "has 8 because it can stop immediately after the string pointer" (fieldalignment). (Both measure 24 bytes via unsafe.Sizeof — same size, different scan cost.)

The strongest version: a struct with no pointer fields at all (only fixed-width numerics/bools/arrays of such) is allocated in a no-scan span the GC never walks. The fastest hot struct is therefore both small and pointer-free — e.g. store an index uint32 into a slab instead of a *T. The allocator-side mechanics (scan vs no-scan spans, the bitmap) are go-perf-allocator-internals; this skill's rule is just: if a struct can be pointer-free, the GC stops touching it.


7. Don't

  • Don't guess a struct's size — measure with unsafe.Sizeof; it includes padding (unsafe).
  • Don't leave a hot, numerous struct in declaration order when fieldalignment says it "could be" smaller — interleaved small/large fields waste bytes (§2).
  • Don't reorder a rare or cold struct for size at the cost of readability — the padding is free there; let methodology gate it (§5).
  • Don't run fieldalignment -fix and skip the tests — it changes field offsets, which breaks unsafe/cgo/encoding/binary/reflection that assumes a layout (§3).
  • Don't assume reordering changes GC cost — it changes size; only removing pointers removes scan work (§6 → go-perf-allocator-internals).
  • Don't blindly pack the most compact order on a concurrently-written struct — adjacent hot fields can land on one cache line and false-share; that needs separation padding, the opposite move (§ → go-perf-false-sharing).
  • Don't Sizeof a slice/map/string and think you measured the data — you measured the header (unsafe).

8. Routing to Related Skills

This plugin (go-perf-* depth):

  • go-perf-allocator-internals — size classes, the tiny allocator, and scan-vs-no-scan spans (why pointer-free structs skip GC scanning).
  • go-perf-false-sharing — padding to separate concurrently-written fields onto distinct cache lines (cpu.CacheLinePad) — the inverse of compacting.
  • go-perf-data-oriented-layout — struct-of-arrays vs array-of-structs, intrusive/ring structures, cache-friendly access patterns.
  • go-perf-methodology — the "is shrinking this struct worth it?" decision and the measure-first gate.

Marketplace (go-* correctness):

  • go-naming-and-style — struct field naming, embedding, tags, idiomatic shape.
  • go-zero-values-and-construction — useful zero values and constructor design (layout serves these, never overrides them).
  • go-slices-and-maps — slice/map header semantics behind the Sizeof-header note.

9. Reference Files

Wrong/right struct-layout mistakes in LLM-generated Go, each with a citation:

${CLAUDE_SKILL_DIR}/references/common-mistakes.md

Source provenance for every claim in this skill:

${CLAUDE_SKILL_DIR}/references/sources.yaml

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.