Skip to content

cl, ssa: fold composite literal stores, support struct slice static inits, and add LTO baseline benchmarks - #2375

Closed
cpunion wants to merge 4 commits into
xgo-dev:mainfrom
cpunion:codex/static-init-struct-slice-on-main
Closed

cl, ssa: fold composite literal stores, support struct slice static inits, and add LTO baseline benchmarks#2375
cpunion wants to merge 4 commits into
xgo-dev:mainfrom
cpunion:codex/static-init-struct-slice-on-main

Conversation

@cpunion

@cpunion cpunion commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

Folds package-level composite literal initializers and struct/composite slice initializers into LLVM static constants, removing runtime allocation loops in init() functions, and adds baseline LTO workload measurements.


Key Changes

  1. Composite Literal & Struct Slice Static Folding (cl/static_init.go):

    • Upgraded staticSliceInitOf and collectAllocStores with recursive collectAddrStores support to handle arbitrary nested struct field address projections and multi-element stores.
    • Converts package-level global struct and composite slice literals (such as []Info{...}) directly into static LLVM constant slice data (p.pkg.ConstSlice), eliminating runtime AllocZ and store loops from package init() functions.
    • Automatically marks all intermediate temporary stack allocs, GEPs, and element stores for suppression from package init().
  2. Zero-Sized Global Init Guard (ssa/decl.go):

    • Added guard ensuring zero-sized global constants (e.g. [0]int{}) emit valid constant zeroinitializers.
  3. Baseline Benchmark Measurements (.github/workflows/benchmark.yml, benchmark/baseline/):

    • Added -lto=full workload measurements (cprintf_lto, println_lto, fmtprintf_lto).
    • Uses dynamic PR merge-base computation for exact baseline comparisons.
  4. Test Coverage (cl/rewrite_internal_test.go):

    • Added TestStaticGlobalStructSliceLiteralInit and comprehensive literal folding test suites.

@cpunion
cpunion force-pushed the codex/static-init-struct-slice-on-main branch from 3c59322 to 72e03db Compare August 20, 2026 08:50
@cpunion
cpunion force-pushed the codex/static-init-struct-slice-on-main branch from 72e03db to d565df7 Compare August 20, 2026 08:54

@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 summary

Solid extension of the static-init folding pass to nested composite literals reached through pointer indirection (*(&localAlloc)), plus a correct fix for the zero-sized-alloc sentinel and useful -lto=full benchmark variants.

Verified sound:

  • The recursive SSA traversal (collectAllocStores / collectAddrStores / handleStoreVal) is guarded well: visited prevents cyclic re-entry (set before recursing), single-referrer/aliasing checks reject shared allocs, appendStaticInitPath defensively copies to avoid path-slice aliasing across sibling branches, and staticInitNode.add rejects conflicting value-vs-children assignments.
  • Traversal is effectively linear in the relevant SSA subgraph; index/field range and array-size caps are enforced.
  • The ssa/decl.go guard is correct: for zero-sized globals doNewVarEx returns a Global aliased to the shared moduleZeroName sentinel, so Init/InitNil must not call SetInitializer on it (the sentinel already has a null initializer under LinkOnceODRLinkage).
  • Doc comments on the new helpers accurately describe behavior, and benchmark/baseline/README.md matches the workloads in main.go.

Note on the Slice case in collectAllocStores: it records nothing into instrs/out, which is fine because its only caller (staticSliceInitOf) pre-seeds {slice, store} into instrs, and nested slices inside structs are rejected upstream (handleStoreVal only accepts *ssa.Const and *ssa.UnOp(MUL)). No data is dropped there.

Findings below are minor robustness/hardening notes; none block merge.

Comment thread ssa/decl.go

// Init initializes the global variable with the given value.
func (g Global) Init(v Expr) {
if g.impl.Name() == moduleZeroName {

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.

[P2] Sentinel guard relies on name-string comparison

Init/InitNil detect the shared zero-sized-alloc sentinel via g.impl.Name() == moduleZeroName. This is correct today, but a pointer/identity comparison against the cached sentinel value (or a boolean flag on aGlobal) would be more robust than a name-string match, since it does not depend on LLVM never renaming the symbol. Functionally the change is correct — the sentinel is created once with a null initializer under LinkOnceODRLinkage, so skipping re-initialization is the right behavior.

Comment on lines +41 to +44
git fetch https://github.com/${{ github.event.pull_request.base.repo.full_name }}.git ${{ github.event.pull_request.base.ref }}
base_sha=$(git merge-base FETCH_HEAD ${{ github.event.pull_request.head.sha }})
echo "sha=$base_sha" >> "$GITHUB_OUTPUT"
echo "Computed pull request merge-base: $base_sha (head: ${{ github.event.pull_request.head.sha }})"

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.

[P3] Untrusted event fields interpolated into a run step

The Determine pull request merge-base step expands ${{ github.event.pull_request.* }} directly into a shell run: block — the canonical GitHub Actions script-injection shape. Severity is low here: the trigger is pull_request (read-only GITHUB_TOKEN, no secrets), head.sha is a hex SHA, and base.repo.full_name/base.ref are base-side (maintainer-controlled for fork PRs). Recommend passing these through env: and referencing quoted shell variables (e.g. "$BASE_REF", "$HEAD_SHA") so the workflow stays safe against future refactors (e.g. to pull_request_target).

@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.

One additional inline note (robustness only; does not block merge). See the main review for the summary.

Comment thread cl/static_init.go
if !ok || elemStore.Val != ref {
return false
}
case *ssa.UnOp:

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.

[P3] UnOp load suppressed without checking its consumer

In the *ssa.UnOp branch of collectAllocStores, the load (*alloc) is required to have exactly one referrer and is then added to *instrs (suppressed), but that single referrer is never inspected or recorded. For the currently reachable patterns this is safe (the consuming store is already tracked/suppressed by the caller), but the function is recursive and reused, so suppressing the load while leaving a live consumer would create an SSA reference to a never-emitted value. Consider verifying unopRefs[0] is (or will be) suppressed, or rejecting the case, to make the invariant explicit rather than implicit.

@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 65.43210% with 56 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
cl/static_init.go 65.82% 36 Missing and 18 partials ⚠️
ssa/decl.go 50.00% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@github-actions

Copy link
Copy Markdown

LLGo baseline benchmarks

d565df753ba9 | 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% 367.204 ms +17.01 ms / +4.9% (worse) 1.425 ms +65.08 us / +4.8% (worse)
Linux cprintf\_lto 19120 B 0 B / +0.0% 367.758 ms +12.7 ms / +3.6% (worse) 1.367 ms -65 us / -4.5% (better)
Linux fmtprintf 1839088 B -21088 B / -1.1% (better) 2.841 s +5.633 ms / +0.2% (worse) 3.607 ms +3.165 us / +0.1% (worse)
Linux fmtprintf\_lto 1759472 B -17648 B / -1.0% (better) 14.497 s -1.078 s / -6.9% (better) 3.791 ms -121.7 us / -3.1% (better)
Linux println 68776 B 0 B / +0.0% 366.139 ms +13.4 ms / +3.8% (worse) 1.967 ms +230 us / +13.2% (worse)
Linux println\_lto 62464 B 0 B / +0.0% 572.664 ms +16.83 ms / +3.0% (worse) 1.733 ms +11.95 us / +0.7% (worse)
macOS cprintf 84672 B 0 B / +0.0% 613.399 ms +283.4 ms / +85.9% (worse) 6.534 ms +4.131 ms / +172.0% (worse)
macOS cprintf\_lto 100912 B 0 B / +0.0% 509.325 ms +176.5 ms / +53.0% (worse) 4.248 ms +2.036 ms / +92.0% (worse)
macOS fmtprintf 1876480 B -16240 B / -0.9% (better) 3.186 s +933.7 ms / +41.5% (worse) 18.622 ms +8.037 ms / +75.9% (worse)
macOS fmtprintf\_lto 1609712 B -16704 B / -1.0% (better) 17.991 s -859.9 ms / -4.6% (better) 7.653 ms +626.4 us / +8.9% (worse)
macOS println 121360 B 0 B / +0.0% 463.323 ms +145.6 ms / +45.8% (worse) 4.723 ms +1.435 ms / +43.6% (worse)
macOS println\_lto 128528 B 0 B / +0.0% 608.700 ms +183.9 ms / +43.3% (worse) 3.036 ms +78.75 us / +2.7% (worse)
Core language and compiler benchmarks
Platform Benchmark ns/op vs base
Linux BenchmarkLookupPCRandom 12.270 ns/op -0.05 ns/op / -0.4% (better)
Linux BenchmarkMergeCompilerFlags 145.900 ns/op -0.6 ns/op / -0.4% (better)
Linux BenchmarkMergeLinkerFlags 94.510 ns/op -2.35 ns/op / -2.4% (better)
Linux BenchmarkChannelBuffered 36.330 ns/op +0.02 ns/op / +0.1% (worse)
Linux BenchmarkChannelHandoff 24296 ns/op +792 ns/op / +3.4% (worse)
Linux BenchmarkDefer 47.400 ns/op +0.91 ns/op / +2.0% (worse)
Linux BenchmarkDirectCall 1.757 ns/op -0.001 ns/op / -0.1% (better)
Linux BenchmarkGlobalRead 1.758 ns/op 0 ns/op / +0.0%
Linux BenchmarkGlobalWrite 2.805 ns/op 0 ns/op / +0.0%
Linux BenchmarkGoroutine 29111 ns/op -1274 ns/op / -4.2% (better)
Linux BenchmarkInterfaceCall 8.654 ns/op -0.024 ns/op / -0.3% (better)
Linux BenchmarkRuntimeGetG 2.112 ns/op +0.001 ns/op / +0.04737% (worse)
macOS BenchmarkLookupPCRandom 13.570 ns/op -3.03 ns/op / -18.3% (better)
macOS BenchmarkMergeCompilerFlags 112.600 ns/op -42.1 ns/op / -27.2% (better)
macOS BenchmarkMergeLinkerFlags 77.320 ns/op -29.18 ns/op / -27.4% (better)
macOS BenchmarkChannelBuffered 23.340 ns/op -2.25 ns/op / -8.8% (better)
macOS BenchmarkChannelHandoff 7993 ns/op +2563 ns/op / +47.2% (worse)
macOS BenchmarkDefer 34.650 ns/op +6.84 ns/op / +24.6% (worse)
macOS BenchmarkDirectCall 1.111 ns/op +0.1605 ns/op / +16.9% (worse)
macOS BenchmarkGlobalRead 1.037 ns/op +0.013 ns/op / +1.3% (worse)
macOS BenchmarkGlobalWrite 1.032 ns/op -0.037 ns/op / -3.5% (better)
macOS BenchmarkGoroutine 54621 ns/op -7899 ns/op / -12.6% (better)
macOS BenchmarkInterfaceCall 4.617 ns/op +0.269 ns/op / +6.2% (worse)
macOS BenchmarkRuntimeGetG 2.868 ns/op +0.038 ns/op / +1.3% (worse)

Compared with 0314faeba69d measured in the same runner job.

@cpunion

cpunion commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator Author

Closing in favor of restructured PRs.

@cpunion cpunion closed this 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