Skip to content

cl: fold slices of struct and array values into static initializers - #2378

Open
cpunion wants to merge 11 commits into
xgo-dev:mainfrom
cpunion:codex/fold-struct-slice-on-static-init
Open

cl: fold slices of struct and array values into static initializers#2378
cpunion wants to merge 11 commits into
xgo-dev:mainfrom
cpunion:codex/fold-struct-slice-on-static-init

Conversation

@cpunion

@cpunion cpunion commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Note

Depends on #2376: this PR extends the alloc-backed global-initializer folding introduced there.

Summary

Extends static initialization to eligible package-level slice literals whose backing arrays contain struct or array values, including nested combinations of those types. Constant elements are emitted into a writable package global and the slice header is installed as the destination global initializer, removing the corresponding runtime allocation and store sequence from package initialization.

Dynamic, shared, bounded, malformed, or otherwise ambiguous SSA shapes continue to use normal runtime initialization.

Implementation

  1. Recursive store collection (cl/static_init.go)

    • Traces stores through nested field and constant-index address projections.
    • Accepts only a full-slice terminal consumed by the expected destination store.
    • Rejects extra loads or slices, executable consumers, dynamic indices or values, duplicate stores, cycles, and unsupported paths.
  2. Static slice construction

    • Groups collected stores by slice element and rebuilds each element with staticInitNode.
    • Uses p.pkg.ConstSlice to create a slice value backed by writable static storage, preserving Go slice mutability.
  3. Regression coverage (cl/rewrite_internal_test.go)

    • Covers simple and nested composite elements.
    • Covers fallback behavior for dynamic fields and indices, bounded or shared slices, extra aggregate consumers, multiple stores, and malformed terminal/path contracts.

Measured Impact

The baseline workflow reports these fmtprintf executable file-size reductions against the main merge base:

  • Linux: -21,088 B without LTO; -17,648 B with full LTO.
  • macOS: -16,240 B without LTO; -16,704 B with full LTO.

All CI and Codecov checks pass.

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fold struct/slice on static init

This PR extends static-init folding to trace constant stores through local stack allocs (nested structs/slices/pointers) and adds -lto=full benchmark workloads. The recursive SSA traversal is generally well-guarded — visited prevents cyclic pointer graphs, appendStaticInitPath correctly allocates fresh backing slices to avoid append-aliasing, staticInitNode.add conservatively aborts on any path collision, and index/field bounds are validated at multiple layers. The ssa/decl.go moduleZeroName guards are correct and well-documented, and the benchmark workflow uses pull_request (read-only token) with persist-credentials: false, so the untrusted-fork surface is contained.

I found one correctness regression that should be fixed before merge (inline). A few lower-priority notes are below.

Lower-priority observations (no inline):

  • Redundant load suppression (cl/static_init.go): the top-level unop is appended to candidate.instrs (~line 155) and is also suppressed inside collectAllocStores via the UnOp case (~line 246), since it is one of the alloc's referrers. Harmless because staticInitInstrs is a map, but the overlapping ownership of suppression between caller and callee is worth consolidating for clarity.

  • UnOp case suppresses any single-referrer load without checking its consumer (cl/static_init.go ~line 238-246): the internal UnOp branch only verifies the load has exactly one referrer before suppressing it, unlike the entry points (~lines 147, 333) which assert unopRefs[0] == store. Given the current single-referrer checks this appears safe, but confirming the consumer is part of the fold would make the suppression contract more robust. Low confidence — flagging for author awareness.

  • O(depth²) path reconstruction (cl/static_init.go, staticInitStorePathToAlloc / appendStaticInitPath): each recursion level re-allocates and copies the accumulated prefix, giving O(d²) copies per store. Impact is bounded by shallow struct/array nesting depth and the 65536-element array cap, so this is minor — noting only as a possible cleanup (thread the prefix down, or size once at the leaf).

  • staticInitPathElem is a single-field struct — a bare int/named int type would simplify the many []staticInitPathElem{{index: ...}} constructions if no further fields are planned. Style only.

Comment thread cl/static_init.go
@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown

LLGo baseline benchmarks

3b3e1120ad02 | workflow run | long-term charts

Program measurements

Platform Workload File size vs base Build vs base Run vs base
Linux cprintf 19288 B 0 B / +0.0% 339.879 ms +4.435 ms / +1.3% (worse) 1.332 ms -43.2 us / -3.1% (better)
Linux cprintf-lto 19120 B 0 B / +0.0% 350.038 ms +11.44 ms / +3.4% (worse) 1.393 ms +41.96 us / +3.1% (worse)
Linux fmtprintf 1839088 B -21088 B / -1.1% (better) 2.778 s -32.59 ms / -1.2% (better) 3.563 ms -212.7 us / -5.6% (better)
Linux fmtprintf-lto 1759472 B -17648 B / -1.0% (better) 14.011 s -1.305 s / -8.5% (better) 3.552 ms -17.48 us / -0.5% (better)
Linux println 68776 B 0 B / +0.0% 343.630 ms +7.415 ms / +2.2% (worse) 1.786 ms -60.46 us / -3.3% (better)
Linux println-lto 62464 B 0 B / +0.0% 576.767 ms +22.02 ms / +4.0% (worse) 1.832 ms +65.6 us / +3.7% (worse)
macOS cprintf 84672 B 0 B / +0.0% 432.317 ms -13.54 ms / -3.0% (better) 3.505 ms +661.5 us / +23.3% (worse)
macOS cprintf-lto 100912 B 0 B / +0.0% 463.077 ms -24.1 ms / -4.9% (better) 3.164 ms -1.237 ms / -28.1% (better)
macOS fmtprintf 1876480 B -16240 B / -0.9% (better) 2.985 s -15.11 ms / -0.5% (better) 16.387 ms +2.05 ms / +14.3% (worse)
macOS fmtprintf-lto 1609712 B -16704 B / -1.0% (better) 17.119 s -4.727 s / -21.6% (better) 6.295 ms -173.7 us / -2.7% (better)
macOS println 121360 B 0 B / +0.0% 422.058 ms -24.8 ms / -5.6% (better) 4.061 ms -87.25 us / -2.1% (better)
macOS println-lto 128528 B 0 B / +0.0% 595.941 ms -51.48 ms / -8.0% (better) 3.825 ms -825.7 us / -17.8% (better)
Core language and compiler benchmarks
Platform Benchmark ns/op vs base
Linux BenchmarkLookupPCRandom 12.410 ns/op +0.07 ns/op / +0.6% (worse)
Linux BenchmarkMergeCompilerFlags 144.500 ns/op -0.1 ns/op / -0.1% (better)
Linux BenchmarkMergeLinkerFlags 94.950 ns/op +0.6 ns/op / +0.6% (worse)
Linux BenchmarkChannelBuffered 36.330 ns/op +0.05 ns/op / +0.1% (worse)
Linux BenchmarkChannelHandoff 25187 ns/op -698 ns/op / -2.7% (better)
Linux BenchmarkDefer 46.800 ns/op +0.7 ns/op / +1.5% (worse)
Linux BenchmarkDirectCall 1.759 ns/op +0.001 ns/op / +0.1% (worse)
Linux BenchmarkGlobalRead 1.758 ns/op +0.001 ns/op / +0.1% (worse)
Linux BenchmarkGlobalWrite 2.808 ns/op +0.003 ns/op / +0.1% (worse)
Linux BenchmarkGoroutine 30725 ns/op -260 ns/op / -0.8% (better)
Linux BenchmarkInterfaceCall 8.637 ns/op +0.094 ns/op / +1.1% (worse)
Linux BenchmarkRuntimeGetG 2.115 ns/op +0.003 ns/op / +0.1% (worse)
macOS BenchmarkLookupPCRandom 13.350 ns/op +1.52 ns/op / +12.8% (worse)
macOS BenchmarkMergeCompilerFlags 139.300 ns/op +16.7 ns/op / +13.6% (worse)
macOS BenchmarkMergeLinkerFlags 82.480 ns/op +8.85 ns/op / +12.0% (worse)
macOS BenchmarkChannelBuffered 23.710 ns/op -0.39 ns/op / -1.6% (better)
macOS BenchmarkChannelHandoff 8313 ns/op +471 ns/op / +6.0% (worse)
macOS BenchmarkDefer 36.240 ns/op -0.17 ns/op / -0.5% (better)
macOS BenchmarkDirectCall 1.065 ns/op -0.118 ns/op / -10.0% (better)
macOS BenchmarkGlobalRead 1.066 ns/op -0.011 ns/op / -1.0% (better)
macOS BenchmarkGlobalWrite 1.146 ns/op +0.043 ns/op / +3.9% (worse)
macOS BenchmarkGoroutine 41701 ns/op -13739 ns/op / -24.8% (better)
macOS BenchmarkInterfaceCall 4.718 ns/op -0.803 ns/op / -14.5% (better)
macOS BenchmarkRuntimeGetG 2.105 ns/op -0.39 ns/op / -15.6% (better)

Compared with 0314faeba69d measured in the same runner job.

@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.65217% with 8 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
cl/static_init.go 95.50% 6 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

@cpunion
cpunion force-pushed the codex/fold-struct-slice-on-static-init branch from affefb0 to d901863 Compare August 20, 2026 12:38
@cpunion

cpunion commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the review follow-ups in d90186357:

  • alloc traversal now owns terminal suppression; callers only add the destination store, removing the duplicate ownership;
  • both aggregate loads and full-slice terminals must be the exact value stored by the expected destination and have that store as their sole non-debug referrer; unrelated loads/slices make the fold fall back;
  • recursive path construction now uses ordinary append instead of copying the whole prefix at every recursion level;
  • retained the named staticInitPathElem wrapper because it keeps path representation explicit and extensible; this is intentionally only a style choice.

Added real-SSA regressions for extra loads, shared slice backing, bounded slices, dynamic fields, nested aggregates, and malformed paths. The targeted static-init tests, zero-sized alias test, benchmark baseline tests, and repository formatting check all pass locally.

@cpunion
cpunion force-pushed the codex/fold-struct-slice-on-static-init branch from d901863 to c2fb0ee Compare August 20, 2026 13:11
@cpunion
cpunion force-pushed the codex/fold-struct-slice-on-static-init branch from c2fb0ee to 1efd309 Compare August 20, 2026 13:44
@cpunion cpunion changed the title cl: support static initializer folding for struct and composite slices cl: fold slices of composite values into static initializers Aug 20, 2026
@cpunion cpunion changed the title cl: fold slices of composite values into static initializers cl: fold slices of struct and array values into static initializers Aug 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant