perf: reduce GC scan span and eliminate padding in hot structs - #683
Merged
Conversation
Audit against docs/development/high-performance-go.md identified padding
waste and GC pointer-scan bloat in five packages. Each fix is guarded by a
red/green test (TestStructLayout, TestRuleFieldOrder, TestStripPrefixNoAlloc)
that fails before the reorder and passes after.
1. listscan: reorder Item/List/frame/markerInfo/parser fields
- Item 40→32 bytes (8 bytes saved per item in per-file slices)
- List 72→64 bytes (8 bytes saved per list)
- frame 56→48 bytes (8 bytes saved per open stack entry)
- markerInfo 40→24 bytes (16 bytes saved per parsed marker line)
- parser: group pointer-bearing slices first, reducing GC scan range
Five rules (MDS014/016/045/046/061) benefit on every file with lists.
2. tablefmt: eliminate double string/byte allocation in stripPrefix
- Previous impl: string(line) assigned to variable + []byte(s[len:]) =
2 allocs per table row when a blockquote/list prefix is present.
- Fix: string(line[:plen]) == prefix (compiler-optimised, 0 allocs) and
return the sub-slice line[plen:] (no copy).
- Same pattern already used in tableformat/structure.go rowContent.
MDS025 carries the highest grandfathered alloc budget (60); every
removed alloc per row compounds across multi-row tables.
3. tablefmt: GC scan reduction for Violation, table, row structs
- Violation: string (pointer) moved before int → scan 16→8 bytes.
- table: string moved between the two slices → scan 56→48 bytes.
- row: isSeparator bool moved after alignments slice → scan 40→32 bytes.
4. crossfilereferenceintegrity: struct field reorderings
- Rule struct: bool fields Strict/Wikilinks moved after slices/string/Links;
size 128→120 bytes (8 bytes saved per Rule instance).
- checkCtx: maps grouped before strings, bool last; GC scan 72→56 bytes.
- wikilinkResolver: pointer fields first, strings last; GC scan 64→56 bytes.
- targetFile: func field first, strings after; GC scan 40→32 bytes.
5. tableformat + toc: GC scan reduction for Rule structs
- MDS025 Rule: Style string moved first; GC scan 24→8 bytes.
- MDS038 Rule: engine pointer moved before engineOnce; GC scan 24→8 bytes.
Ref: docs/development/high-performance-go.md (struct layout, GC scan, allocs)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pKxVjp82WDkwB4MKNNKM8
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files
☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Under -race, race instrumentation inflates AllocsPerRun counts, causing the zero-alloc assertion to fail spuriously. All three existing alloc tests in tablefmt_test.go already skip under -race; align this new test with that pattern. Also move the escape sink from package-level to function-local to avoid cross-test variable sharing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pKxVjp82WDkwB4MKNNKM8
sync.Once is 12 bytes (atomic.Bool 4 + sync.Mutex 8), not 16. The old layout still produces a 24-byte GC scan span because 4 bytes of alignment padding push the engine pointer to offset 16. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pKxVjp82WDkwB4MKNNKM8
The comment described the struct as "currently packs bool fields between larger fields" — present tense that was accurate before the reorder but misleads future readers once the fix is applied. Rewrite to past tense. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pKxVjp82WDkwB4MKNNKM8
Owner
Author
|
🟢 Merge Queue — picked up This PR is in the queue and will be batched with other Next: No action needed — you'll get another comment when CI starts on the batch. View merge queue run. |
Owner
Author
|
🔵 Merge Queue — CI running Merged into batch branch Next: No action needed — you'll be notified when CI completes. |
Owner
Author
|
✅ Merge Queue — merged This PR landed on Next: Done — nothing more to do here. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Addresses struct layout violations identified by auditing against
docs/development/high-performance-go.md. Each fix reduces GC pointer-scan span or eliminates alignment padding in structs that are allocated per-lint-call, reducing GC pressure on the hot path. All fixes follow Red/Green TDD: failing layout/alloc test → struct reorder or function rewrite → passing test → commit.Fixes
tablefmt.stripPrefix— zero-alloc rewrite: The previous implementation calledstring(line)and[]byte(…), causing 2 heap allocations per table row. Replaced with astring(line[:plen]) == prefixslice comparison the compiler optimises to a zero-alloc memcmp. Guarded byTestStripPrefixNoAlloc(skipped under-racewhere instrumentation inflates alloc counts).listscanstructs — padding elimination: ReorderedItem(40→32 bytes),List(72→64 bytes),frame(56→48 bytes), andmarkerInfo(40→24 bytes) to group bools together at the end, eliminating bool-induced alignment padding. Asserted byTestStructLayoutviaunsafe.Sizeof.crossfilereferenceintegritystructs — GC span + padding: ReorderedRule(128→120 bytes, bools moved last),checkCtx(GC span 72→56, maps before strings),wikilinkResolver(GC span 64→56, interface+pointer before strings), andtargetFile(GC span 40→32, func field first). Asserted byTestStructLayoutviaunsafe.Sizeof.tableformat.Rule— GC scan span: MovedStyle stringfirst so the GC pointer-scan span is 8 bytes (string data pointer at offset 0) rather than 24 bytes (string after two int fields). Asserted byTestRuleFieldOrderviaunsafe.Offsetof.toc.Rule— GC scan span: Movedengine *gensection.EnginebeforeengineOnce sync.Onceso the GC pointer-scan span is 8 bytes (pointer at offset 0) rather than 24 bytes (pointer buried after the 12-byte sync.Once padded to 16 for alignment). Asserted byTestRuleFieldOrderviaunsafe.Offsetof.Post-review fixes (3 rounds of
/code-review --effort xhigh)raceEnabledguard totablefmt/layout_test.go—testing.AllocsPerRunreturns inflated counts under-racedue to instrumentation overhead; existing tablefmt tests use the same guard pattern.toc/layout_test.gocomment:sync.Onceis 12 bytes in Go 1.25, padded to 16 to align the subsequent pointer, giving a 24-byte scan span in the old layout.crossfilereferenceintegrity/layout_test.gothat described the struct as "currently packs bool fields" after the reorder was already applied.Test plan
go test ./internal/rules/listscan/...—TestStructLayoutpasses at 32/64/48/24 bytesgo test ./internal/rules/tablefmt/...—TestStripPrefixNoAllocallocates 0go test ./internal/rules/crossfilereferenceintegrity/...—TestStructLayoutpasses at 120 bytesgo test ./internal/rules/tableformat/...—TestRuleFieldOrderpasses at offset 0go test ./internal/rules/toc/...—TestRuleFieldOrderpasses at offset 0go test ./...— full suite greenReference:
docs/development/high-performance-go.md🤖 Generated with Claude Code
https://claude.ai/code/session_015pKxVjp82WDkwB4MKNNKM8