Skip to content

About

Detect and fix suboptimal struct field layout in Go: reduces padding and GC pointer bytes, rewrites files while preserving comments. A drop-in fieldalignment replacement.

Topics

Resources

Stars

1.1k stars

Watchers

9 watching

Forks

Latest commit

 

History

196 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

betteralign

GitHub license GitHub release go-recipes

About

betteralign detects and fixes suboptimal struct field layout in Go: it reduces padding and GC pointer bytes, and rewrites files while preserving comments. It is a drop-in replacement for fieldalignment.

It is a fork of the official Go fieldalignment tool, with the bulk of the alignment logic unchanged. Its notable differences group into a few themes:

Why this exists

  • preserves comments (field comments, doc comments, and floating comments) — the original fieldalignment erases them on rewrite.

Safety and robustness

  • respects layout contracts: structs carrying a structs.HostLayout marker or explicit blank padding (_ [56]byte cache-line pads, _ uint32 alignment pads) are left alone entirely, and structs the package depends on in field order are reported but never rewritten — those used with unsafe.Offsetof or a 64-bit sync/atomic function (atomic.AddInt64(&s.n, 1) needs n 8-byte aligned on 32-bit platforms), converted to or from another struct type, identical to an unnamed struct type (or alias) they may be assigned to, type-punned through unsafe.Pointer, encoded with encoding/binary, encoding/asn1 or x/crypto/ssh, or passed to an assembly function (a body-less declaration); structs that declare themselves ASN.1 (an asn1:"…" tag or an encoding/asn1 field type, also in a struct they hold by value) are skipped outright,
  • detects positional composite literals (T{1, 2, 3} rather than T{a: 1, b: 2, c: 3}) anywhere in the package and reports the would-be saving without rewriting the struct — reordering its fields would silently re-map the literal's elements at the next build,
  • analyzes only named struct types — declared with type T struct { ... }, or an alias of a struct literal (type A = struct { ... }); anonymous structs (nested struct-typed fields, struct literals, var x struct{...} declarations) are skipped, so it never rewrites the riskier unnamed shapes,
  • when -apply rewrites a struct, fields declared with multiple names (A, B int) are kept together as a single grouped declaration and moved as a unit — the byte-span reorder never splits them,
  • performs atomic file writes to prevent corruption or data loss on rewrite (not on Windows),
  • uses a stable sort for field ordering, so fields of equal sort rank preserve their original relative order; upstream's sort.Slice is unstable and may permute equal-ranked fields differently between runs,
  • fixes a crash on packages with generic struct fields — upstream's ptrdata implementation ends with panic("impossible"), which fires when an uninstantiated type parameter appears as a field type (because (*types.TypeParam).Underlying() returns itself, falling through every switch case); betteralign replaces the panic with a conservative one-word fallback.

Opt-in / opt-out controls

  • skips generated files, identified either by a known suffix (_generated.go, _gen.go, .gen.go, .pb.go, .pb.gw.go) or by a package-level comment containing Code generated by ... DO NOT EDIT. (or any header the Go convention, ast.IsGenerated, accepts),
  • skips test files (_test.go suffix),
  • skips structs annotated with // betteralign:ignore placed inside the struct body,
  • supports opt-in mode, where only structs annotated with // betteralign:check on the type declaration are checked; placing the directive on a surrounding type ( ... ) block opts in every spec inside the group.

Other improvements

  • caches per-type size, alignment, and pointer-scan results within each analysis pass, avoiding redundant recomputation for types that recur across many struct fields (common in protobuf-generated code),
  • prefixes diagnostics with the actual byte saving (8 bytes saved: struct of size 24 could be 16) rather than only the before/after sizes,
  • includes more thorough tests comparing expected versus golden output,
  • adapts automatically to CPU and memory constraints in containerized environments (Docker, Kubernetes, LXC, LXD, etc.).

Comment preservation is achieved by a small in-tree package, internal/dstmin, that decorates the parser's *ast.File with byte-range spans for each field's lead-doc, body and trailing blanks, then reprints the file by byte-splicing the synthesized struct bodies into the original source and finalising with go/format.Source for column realignment. It replaces a previous dependency on sirkon/dst with ~900 lines of focused code; see internal/dstmin/README.md for the design rationale and benchmark comparison. Each reordered struct's gofmt'd body is then cut out of that reprint and attached to its diagnostic as a standard SuggestedFix (titled Rearrange fields, like upstream), so -fix works through the regular analysis driver, and other tools that apply analysis suggested fixes (for example a multichecker build bundling betteralign) get the same comment-preserving reorder. Each edit already carries gofmt's indentation and alignment, so it is correct even when applied without a separate formatting pass.

Go's standard AST does not associate comments with nodes — it only stores byte offsets, so the original fieldalignment tool erases all comments. A proposed fix exists as an open CL but has not yet been merged.

Note: One run reaches a fixed point: nested structs are measured at the layout the same run gives them (see Nested structs), so running -fix or -apply again changes nothing.

This tool builds upon the following prior work:

Deep dive

betteralign pursues two goals:

  1. Minimize struct size by sorting fields in descending alignment order and placing zero-sized fields first, reducing internal padding (and avoiding the one-byte tail the runtime adds when a struct ends in a zero-sized field).
  2. Lower the ptrdata-based scan-work estimate the GC uses for pacing by grouping pointer-bearing fields before pointer-free ones, shrinking each value's ptrdata (the head prefix the runtime records as potentially holding pointers). On Go before 1.26 this also directly reduced the bytes scanned per value, since the collector stopped scanning at the last pointer in a value; under the Green Tea GC (default in Go 1.26) marking is span-based rather than per-value, so the win is the smaller ptrdata feeding GC pacing rather than fewer bytes physically scanned.

With those goals in mind, fields are sorted stably by the following criteria, in order:

  1. Zero-sized types first.
  2. Higher alignment first.
  3. Pointer-bearing types before pointer-free types.
  4. Among pointer-bearing types, those with fewer trailing non-pointer bytes first (minimizing GC ptrdata).
  5. Larger size first.

Size: sorting by descending alignment

type Record struct {
	Flag  bool  // 1 byte,  1-byte aligned
	ID    int64 // 8 bytes, 8-byte aligned
	Count int32 // 4 bytes, 4-byte aligned
}

As declared, Record is 24 bytes on a 64-bit platform: Flag sits at offset 0, then 7 padding bytes appear so ID can land on an 8-byte boundary; Count follows at offset 16, and the struct is rounded up to the next multiple of its highest alignment (8), adding 4 trailing padding bytes.

After optimalOrder sorts by descending alignment, the layout becomes ID, Count, Flag and shrinks to 16 bytes: ID at offset 0, Count at offset 8, Flag at offset 12, plus 3 trailing padding bytes to round to 16. No internal padding is needed because each field already lands on a correctly-aligned offset. Same data, 33% less memory.

GC ptrdata: ordering pointer-bearing fields by trailing non-pointer bytes

type IPAddr struct {
	Zone string // 16 bytes, 8-byte aligned
	IP   []byte // 24 bytes, 8-byte aligned
}

Zone comes first because string has fewer trailing non-pointer bytes than []byte: Sizeof(string) - ptrdata(string) = 8 versus Sizeof([]byte) - ptrdata([]byte) = 16. The struct is 40 bytes either way on a 64-bit platform, but with Zone first the value's ptrdata is only 24 bytes; reversing the fields would push it to 32. That ptrdata feeds the GC's scan-work estimate used for pacing — and, on Go before 1.26, the number of bytes the collector scanned per value. Under the Green Tea GC (default in Go 1.26) marking is span-based, so the smaller ptrdata lowers the pacing estimate rather than the bytes physically scanned.

The pointer-bytes diagnostic reflects this: betteralign reads the Go version of the project being analyzed (its go.mod go directive, or a per-file //go:build go1.x constraint). For a target on Go 1.26 or newer it reports the reorder as lowering the GC scan-work estimate; for older targets it keeps the classic N bytes saved framing, which there still maps to bytes physically scanned. Struct-size diagnostics are unaffected — that saving is real memory regardless of GC.

Nested structs

A struct held by value inside another is measured at the layout the same run gives it: if Inner is reordered, Outer is laid out around the reordered Inner, so a single -fix or -apply reaches a fixed point — running it again changes nothing, and a follow-up check passes. Diagnostic figures are in the same terms: an outer struct's size and pointer bytes assume its inner structs already reordered.

Runtime tuning with GOGC

The analyzer itself is allocation-light, but loading and type-checking a package graph (go/types, the AST inspector, and the parser) produces a large volume of short-lived garbage — on a real-world run, well over 1 GiB of churn against only a few MiB of live heap at exit. Because betteralign is a short-lived batch process, the default GOGC=100 spends CPU collecting garbage that would be freed at exit anyway. Running with garbage collection disabled trades that work for higher peak memory:

GOGC=off betteralign ./...

On a large warm-cache run this measured roughly a 15–16% wall-clock improvement (and GOGC=400 about 13%), since most of the runtime's GC work simply disappears.

Caution

GOGC=off lets the heap grow unbounded for the duration of the run. In memory-constrained containers (e.g. a Kubernetes pod with a low memory limit) this can trigger an OOM kill on a large package graph. betteralign already sets a soft GOMEMLIMIT (90% of the cgroup/system memory) which acts as a backstop, but on tight limits prefer a high finite value such as GOGC=400 over fully disabling collection, or leave the default.

Installation

Manual: Download the appropriate binary from the releases page and place it in your PATH, typically /usr/local/bin/betteralign.

Via go install:

go install github.com/dkorunic/betteralign/cmd/betteralign@latest

Usage

betteralign: find structs that would use less memory if their fields were sorted

Usage: betteralign [-flag] [package]

This analyzer finds structs that can be rearranged to use less memory, and provides
a suggested edit with the most compact order.

Note that there are two different diagnostics reported. One checks struct size,
and the other reports "pointer bytes" used. Pointer bytes is how many bytes of the
object that the garbage collector has to potentially scan for pointers, for example:

        struct { uint32; string }

have 16 pointer bytes because the garbage collector has to scan up through the string's
inner pointer.

        struct { string; *uint32 }

has 24 pointer bytes because it has to scan further through the *uint32.

        struct { string; uint32 }

has 8 because the pointer bytes end immediately after the string pointer.

This "pointer bytes" figure is the type's ptrdata. On Go before 1.26 it was
also the number of bytes the collector scanned per value; under the Green Tea
GC (default in Go 1.26) marking is span-based rather than per-value, so a
smaller ptrdata lowers the ptrdata-based scan-work estimate the runtime uses
for GC pacing rather than the bytes physically scanned. betteralign frames the
pointer-bytes diagnostic to match the Go version of the analyzed package.

Be aware that the most compact order is not always the most efficient.
In rare cases it may cause two variables each updated by its own goroutine
to occupy the same CPU cache line, inducing a form of memory contention
known as "false sharing" that slows down both goroutines.

Unlike most analyzers, which report likely mistakes, the diagnostics
produced by betteralign very rarely indicate a significant problem,
so the analyzer is not included in typical suites such as vet or
gopls. Use this standalone command to run it on your code:

   $ go install github.com/dkorunic/betteralign/cmd/betteralign@latest
   $ betteralign [packages]

Only named struct types are analyzed: those declared with the
`type T struct { ... }` form, and aliases of a struct literal
(`type A = struct { ... }`). Anonymous structs (nested struct-typed fields, struct literals,
`var x struct{...}` declarations, and similar unnamed forms) are
skipped. To enable analysis of one of those, lift it into a named type
declaration.

A generic struct whose layout depends on a type parameter — one with a type
parameter in a by-value field position (`B T`, `B [4]T`, or a by-value
struct holding one) — is also skipped. Such a field is sized from the
constraint's underlying (`T any` as `interface{}`), so its size and
pointer-bytes figures would be per-instantiation guesses. Fields that hold a
type parameter only indirectly (`*T`, `[]T`, `map[K]T`) have a fixed
layout and are still analyzed.

If a struct is constructed somewhere in the package with a positional
composite literal (`T{1, 2, 3}` rather than `T{a: 1, b: 2, c: 3}`),
the reorder is reported but never applied: rewriting the field order would
re-map the literal's elements to different fields, breaking the build (or
worse, silently mis-assigning values when the new field types still happen
to accept the old element types). Convert the literal to keyed form and
rerun to enable the reorder.

This positional-literal guard only sees the package that declares the struct
and its in-package tests. A positional literal of an exported struct used from
another package (an ordinary importer, or an external `package p_test`) is
invisible to the pass that rewrites the struct, so `-apply` may reorder it and
break — or silently mis-assign — that caller. Unexported structs are safe. When
running `-apply ./...` across a multi-package module, gate it with a
`go build ./...` (and your tests) afterwards, or convert positional literals
to keyed form first. The in-package test guard also relies on the test files
being loaded, so a driver run with -test=false can still rewrite a struct
pinned by a literal in an in-package test.

A struct with a `structs.HostLayout` field declares that its layout must
follow the platform's C ABI, so it is neither reported nor rewritten. A struct
with explicit padding (a blank `_` field of non-zero size, such as a
cache-line pad) is skipped the same way, since its layout is deliberate.

A struct the package depends on in field order is reported but never
rewritten: one whose field offset is taken with `unsafe.Offsetof`, one
whose field is passed to a 64-bit `sync/atomic` function (which needs it
8-byte aligned on 32-bit platforms), one converted to or from another struct
type (also through a type parameter), one identical to an unnamed struct
type it may be assigned to (including an alias `type A = struct{...}` and a
generic instance), one
converted to or from `unsafe.Pointer` (type punning), and one passed to
`encoding/binary`, `encoding/asn1` or `x/crypto/ssh` (whose wire format is field order),
and one taken or returned by a body-less function declaration, whose
assembly reads fields at fixed offsets. Reordering would break the build,
corrupt memory or silently change the encoding. The last four also cover
structs held by value inside them;
the atomic pin covers every struct that holds such a struct by value. A
struct that declares itself ASN.1 — an `asn1:"..."` tag or a field of an
`encoding/asn1` type (or a slice or array of one), on it or on a struct it holds by value — is skipped outright, since its encoder usually lives
in another package; so is one tagged for `x/crypto/ssh` (`ssh`, `sshtype`). A conversion to a struct of another package in the same
module follows that package: both are reordered, identically.

Each rewritable diagnostic carries a suggested fix, so `-fix` (and
`-fix -diff` to preview) rewrites through the standard analysis driver.
`-apply` rewrites the files itself with atomic writes; under it, `-diff`
prints a unified diff of each rewrite to stdout (stderr with `-json`)
instead of updating the files. `-fix` takes precedence over `-apply`.
The driver never applies fixes to files marked as generated, so with
`-generated_files`, `-fix` instead rewrites through the `-apply` path
(except under `go vet`, where the flag has no effect).
A file that imports `"C"` is analyzed only with `-generated_files` and is
never rewritten.

A file containing carriage returns anywhere (CRLF line endings, even on a
single line) is reported but never rewritten; normalise it to LF to enable
the reorder. Files with a `//line`
directive are reported but never rewritten either: moving lines would break
the mapping, and other packages could not predict the rewrite.



Flags:
  -V    print version and exit
  -all
        no effect (deprecated)
  -apply
        apply suggested fixes
  -c int
        display offending line with this many lines of context (default -1)
  -cpuprofile string
        write CPU profile to this file
  -debug string
        debug flags, any subset of "fpstv"
  -diff
        with -fix, don't update the files, but print a unified diff
  -exclude_dirs value
        exclude directories matching a pattern
  -exclude_files value
        exclude files matching a pattern
  -fix
        apply all suggested fixes
  -flags
        print analyzer flags in JSON
  -generated_files
        also check and fix generated files
  -json
        emit JSON output
  -memprofile string
        write memory profile to this file
  -opt_in
        opt-in mode on per-struct basis with 'betteralign:check' in comment
  -source
        no effect (deprecated)
  -tags string
        no effect (deprecated)
  -test
        indicates whether test files should be analyzed, too (default true)
  -test_files
        also check and fix test files
  -trace string
        write trace log to this file
  -v    no effect (deprecated)

To check all packages in the current module:

betteralign ./...

To automatically rewrite files (excluding test and generated files):

betteralign -apply ./...

Or let the standard analysis driver apply the suggested fixes:

betteralign -fix ./...

To preview the rewrite as a unified diff without touching any file, use either:

betteralign -fix -diff ./...
betteralign -apply -diff ./...

-apply -diff prints gofmt's format — diff X.orig X, --- X.orig, +++ X — with X relative to the working directory when the file is under it (absolute otherwise) and slash-separated on every platform, so a diff made on Windows applies with git apply, and exits 3 when there are diagnostics, like any report. Run from that directory, patch -p0 < out.diff or git apply -p0 out.diff applies it. -fix -diff prints the driver's format instead, absolute paths with (old)/(new) headers, and exits 0.

With -apply -json -diff, the diff goes to stderr so stdout stays valid JSON. Both paths produce the same result; -apply writes each file atomically, while -fix follows the driver's semantics (merging fixes across package variants and reporting any it had to skip). -fix takes precedence if both are given. With -apply (and no -diff) the analyzer writes the files itself and attaches no suggested fixes, so an external driver that applies the fixes it collects must not also set it: use one or the other, not both. -fix output is never JSON: the driver returns before printing JSON under -fix, so -fix -json prints nothing and -fix -diff -json prints a plain diff.

Under go vet -vettool=$(which betteralign), prefer the driver's fixes (go vet -vettool=… -fix, or go fix -fixtool=…): -apply is refused with an error. go vet runs each package in its own process, so a dependency's process rewrites its file before an importer's process predicts from it; the importer no longer finds the struct it converts to, keeps its own side, and the module stops building. To preview, use go vet -vettool=… -fix -diff (go vet rejects -diff without -fix). Under go vet, -fix -generated_files is plain -fix: the driver refuses fixes in generated files, so the flag has no effect there and betteralign says so on stderr; run betteralign directly to rewrite generated files. go vet analyzes each package in its own process, so a conversion to a struct in another package of the module follows that struct when both packages are in the vet pattern; vetting only the importing package (go vet -vettool=… -fix ./user) reorders it while the partner stays, and the conversion no longer compiles — vet the whole module.

Generated and test files can be included with the -generated_files and -test_files flags respectively. Use -exclude_dirs and -exclude_files to skip specific paths. For -exclude_dirs, a separator-free pattern (vendor, internal, test*) matches a directory of that name at any depth, gitignore-style, while a pattern containing a separator (a/internal) matches only that exact path relative to the working directory. -exclude_files patterns match the file's path relative to the working directory, so p.go does not exclude p/p.go; use p/p.go or */p.go. A malformed pattern ([) is an error, not a silent exclude-everything. Use -opt_in to check only structs explicitly annotated with // betteralign:check.

Structs constructed via positional composite literals are reported but never rewritten under -apply; the diagnostic points at the offending literal so it can be converted to keyed form (T{Field: value}), after which a rerun will enable the reorder.

Limitations

  • Contracts in files your build excludes are partly invisible. Positional literals and body-less (assembly) function declarations in the package's files for another GOOS/GOARCH or behind an unset build tag are found syntactically and pin their struct (so a rewrite on Linux no longer breaks the Windows build, or the arm64 assembly behind an f_arm64.go stub); a file under //go:build ignore, which no build compiles, doesn't count. Anything that needs type information there — unsafe.Offsetof, conversions, unsafe.Pointer, encoding/binary — is not seen; run betteralign under each GOOS/GOARCH and tag set you ship, or mark such structs // betteralign:ignore.
  • Cross-package positional literals are not detected. The positional-literal guard sees only the package that declares the struct plus its in-package tests. A positional literal of an exported struct used from another package (an ordinary importer, or an external package p_test) is invisible to the pass that rewrites the struct, because Go's static-analysis facts flow with the import graph, not against it. If such a literal exists, -apply (or -fix) will reorder the struct and either break the build or — when the reordered field types still accept the old element types — silently mis-assign values with a clean compile. This is the most dangerous outcome the tool can produce. Unexported structs are safe (their positional usage is confined to the defining package). When running -apply ./... across a multi-package module, gate it with a go build ./... (and your test suite) afterwards, or convert exported structs' positional literals to keyed form first.
  • In-package test literals require the test variant to be loaded. The deferral that protects a positional literal living only in an in-package *_test.go leaves the rewrite to the [pkg.test] variant whenever the package directory holds an in-package test file this build compiles. Running with the driver's -test=false (which loads no variant) turns it off: the base pass then keeps the rewrite, so -apply or -fix may rewrite a struct pinned by such a literal and break the test build. Leave test loading enabled when applying fixes. File arguments (betteralign -apply p/p.go) load the package as command-line-arguments, which never has a test variant: when the directory holds an in-package test the rewrite stays deferred and betteralign says so on stderr, so run on the package (./p) to rewrite.
  • A struct pinned by an in-package test literal or unsafe.Offsetof may be reported twice. Only the rewrite (-apply and the suggested fix) is deferred to the [pkg.test] variant, not reporting — so healthy code stays covered even when a broken test file makes the driver skip that variant. When the variant does run, both it and the base pass report the struct: the base plain, the variant with the reorder skipped caveat. The caveated one is authoritative; neither rewrites the struct.
  • Files that use cgo (import "C") are not analyzed by default. The driver hands analyzers cgo's generated translation of such files, marked Code generated by cmd/cgo; DO NOT EDIT., so they are skipped as generated code; with -generated_files they are analyzed and reported, but never rewritten (the translation lives in the build cache). Other files in the same package are analyzed and fixed normally.
  • Files containing carriage returns are report-only. A file with a \r anywhere — CRLF line endings, even on a single line — is reported but never rewritten, by -apply or a suggested fix: gofmt, which finishes every rewrite, would normalise all of its line endings. Normalise to LF to enable the rewrite.
  • A leading UTF-8 byte-order mark is dropped by a rewrite. -apply and the driver's -fix both gofmt the rewritten file, which removes it; a suggested fix applied raw by an editor keeps it.
  • Generic structs whose layout depends on a type parameter are skipped. A field holding a type parameter by value (B T, B [4]T, or a by-value struct containing one) is sized from the constraint's underlying (T any becomes interface{}), so any size or pointer-bytes figure would be a per-instantiation guess. Such structs are neither reported nor rewritten. Type parameters held only indirectly (*T, []T, map[K]T) have a fixed layout and are still analyzed.
  • Only some layout contracts are visible. betteralign skips structs with a structs.HostLayout field, a blank padding field of non-zero size, or an ASN.1 marking, and reports without rewriting those whose field order the package itself depends on: unsafe.Offsetof, 64-bit sync/atomic functions on a field, struct-to-struct conversions, identity with an unnamed struct type (including one in an imported package's API, such as a func F(x struct{...}) parameter), unsafe.Pointer punning, encoding/binary/encoding/asn1/x/crypto/ssh arguments and the parameters, results and receivers of body-less (assembly) function declarations (not one a two-argument //go:linkname directive binds to Go code of the same package; a one-argument directive, or a pull from another package, which keeps its own copy of the struct, still pins), found anywhere in the package or its in-package tests (a use only in a test may be reported twice, like a positional literal). The ASN.1 marking deliberately over-suppresses, since it also counts structs held by value: crypto/x509.Certificate (40 bytes to save) is skipped because it holds crypto/x509/pkix types by value, although the standard library never passes it to asn1.Marshal. No // betteralign:ignore is needed for such structs; they just stay unreported. These checks see one package and its imports, never its importers: punning or encoding an exported struct in another package is invisible, as is a value reaching an encoder only through an interface{}. A conversion to another package's struct is predicted (same module or go.work workspace ⇒ both reordered identically; from a go.work root that is not itself a module the go command rejects ./... — "directory prefix . does not contain modules" — so name the modules, ./a/... ./b/..., to have partners across them reorder in step), which reads that package's source for an ignore directive, a generated-code header or an excluded path; a package the run does not analyze (betteralign -apply ./user without ./dep), which the driver loads from export data, is never rewritten, so the local side stays pinned. The prediction can still miss when the struct is pinned there (by a positional literal, unsafe.Offsetof, a 64-bit atomic on a struct it holds, ...), or when that package's in-package _test.go is ill-typed: the driver then skips the [p.test] variant that owns its rewrite, so its struct stays while the importing side follows the prediction (as in v0.15.4). The conversion then fails to compile after the rewrite, until the struct is unpinned or the test file fixed. The 64-bit atomic pin likewise sees only the package doing the atomic access: a struct holding another package's atomically accessed struct by value can still be reordered so that the word loses its 32-bit alignment. Other reflection-based encoders whose format is field order (third-party binary codecs) are not recognised; encoding/json and encoding/xml match by name, though their output order follows the new field order, as does fmt's %v/%+v and that of reflection-based dumpers such as go/ast.Print (tests comparing such strings need updating: go/ast's ExamplePrint fails after rewriting go/ast).
  • A pinned struct held inside another is measured as if reordered. So that one run reaches a fixed point, a struct held by value inside another is measured at the layout the run leaves it in. That prediction must agree across packages (both sides of a cross-package conversion must sort identically), so it can't see a package's own pins: an inner struct pinned by a positional literal, unsafe.Offsetof and the like is measured as reordered although it is left alone, and its enclosing struct's figures and order follow from that. The code stays correct, but the reported figures can be off: when an inner struct is pinned in its own package, the real pointer bytes after the rewrite can be higher than reported, and in rare cases higher than before (in the standard library compress/gzip.Header goes from 72 to 80, since time.Time is pinned in time, and crypto/tls.clientHandshakeState from 192 to 208, since its inner finishedHash is pinned by a positional literal in crypto/tls). Size is never made worse in practice. Ignored, single-line, generated and excluded inner structs are measured as declared. With -opt_in, every inner struct is measured as declared.
  • Never run it on the Go toolchain's own packages. runtime, internal/abi, reflect and friends share struct layouts with the compiler and linker (function tables, module data, type descriptors), and testing's types are built by the positional literals in the test main that go test generates. Rewriting the standard library builds and vets cleanly, but those packages then fail at run time — a contract no analysis of the source can see. The same applies to any struct whose layout a code generator or another binary relies on: mark it // betteralign:ignore. Anything else — cgo C-struct mirrors without structs.HostLayout, syscall/ABI layouts, memory-mapped or wire-format structs, fields accessed by index via reflect, hard-coded offsets or pointer arithmetic, Offsetof in another package — is reordered by -apply/-fix and can silently break. Mark these with // betteralign:ignore (or use -opt_in) before running -apply or -fix.

Star history

Star History Chart

About

Detect and fix suboptimal struct field layout in Go: reduces padding and GC pointer bytes, rewrites files while preserving comments. A drop-in fieldalignment replacement.

Topics

Resources

Stars

1.1k stars

Watchers

9 watching

Forks

Releases

Packages

Used by

Contributors

Languages