Skip to content

cl, ssa: fix zero-sized global init and fold composite literal alloc stores - #2372

Closed
cpunion wants to merge 2 commits into
xgo-dev:mainfrom
cpunion:codex/static-init-composite-literal
Closed

cl, ssa: fix zero-sized global init and fold composite literal alloc stores#2372
cpunion wants to merge 2 commits into
xgo-dev:mainfrom
cpunion:codex/static-init-composite-literal

Conversation

@cpunion

@cpunion cpunion commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

  1. Zero-Sized Global Variable Initializer Guard (ssa/decl.go):

    • Prevent packages with zero-sized global variables (such as internal/poll) from overwriting the module's shared sentinel @"__llgo.moduleZeroSizedAlloc$" with non-i8 initializers during package global initialization, avoiding LLVM module verification errors.
  2. Recursive Composite Literal Static Folding (cl/static_init.go):

    • Trace and fold local temporary struct/array alloc stores for package-level composite literals back into static LLVM global constants, avoiding runtime init() stack allocation, memset, and element-wise store overhead.
    • Use safe path isolation (appendStaticInitPath) to prevent slice-aliasing hazards during DAG path collection.
  3. Unit Tests (cl/rewrite_internal_test.go):

    • Added assertions to verify composite literals static folding and path correctness.

@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: static-init composite literal folding

This PR extends static-init folding to composite literals reached through pointer indirection (&Alloc + UnOp deref), via a new recursive SSA traversal collectAllocStores, plus a moduleZeroName sentinel guard in ssa/decl.go. The overall structure closely mirrors the careful single-use validation of the existing staticSliceInitOf path, and the sentinel guard correctly prevents clobbering the shared zero-sized initializer.

One correctness gap is worth addressing before merge (see inline): the *ssa.UnOp deref branch suppresses a whole-aggregate load without validating that the loaded value is consumed only by the folded store. Verified findings:

  • Suppression is driven by p.staticInitInstrs; compileInstr (cl/compile.go:1761) returns early for suppressed instructions, and a later live consumer routes through compileInstrOrValue(asValue=true)log.Panicln("unreachable:", iv) (cl/compile.go:1309).

Test coverage: collectAllocStores / handleStoreVal / staticInitStorePathToAlloc are not exercised by any new test. Existing static-init tests cover only direct FieldAddr/IndexAddr-to-global stores, not the &Alloc + UnOp deref shape this PR targets. Please add an IR-level test that folds the pointer-indirection form and asserts no double-emission, plus a negative test for the multi-consumer case described inline.

Performance and the sentinel guard were reviewed and found sound; the visited map bounds recursion and array materialization stays capped by maxStaticInitArrayElements.

Comment thread cl/static_init.go
Comment on lines +218 to +222
case *ssa.UnOp:
if ref.Op != token.MUL {
return false
}
*instrs = append(*instrs, ref)

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.

[P1] UnOp deref branch suppresses a load without validating its other uses

This branch accepts any *alloc load and appends it to instrs (marking it static-init-consumed) without checking its referrers. Every other branch here validates single-use first: FieldAddr/IndexAddr require len(refs) == 1 and that the sole referrer is the matching Store; the slice path (staticSliceInitOf) enforces the same via nonDebugReferrers.

If the same alloc's loaded value also flows to a non-static consumer (e.g. a second store, or a function argument), the alloc's stores get folded into a constant while the load is suppressed — but the surviving runtime instruction still references it. Suppressed instructions are skipped in compileInstr (cl/compile.go:1761) and never populate bvals; a later live consumer then hits log.Panicln("unreachable:", iv) (cl/compile.go:1309), or reads uninitialized storage.

Recommend mirroring the slice/field paths: reject unless nonDebugReferrers(ref) has no user other than the driving global store. A negative test for the multi-consumer case would lock this down.

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.

[P3] Document why Init/InitNil no-op for the zero-sized sentinel

Init and InitNil now silently no-op when the global is the shared moduleZeroName sentinel. The rationale (the sentinel is LinkOnceODR and shares its ConstNull initializer across all zero-sized globals, so a per-variable SetInitializer would clobber it for every other zero-sized global) is not obvious from the guard alone. A one-line comment would make the silent behavior discoverable, consistent with the prose already in doNewVarEx. The Init doc comment ("initializes the global variable with the given value") is also now slightly inaccurate for this case, and InitNil has no doc comment where its sibling does.

@cpunion
cpunion force-pushed the codex/static-init-composite-literal branch from c091a40 to 335c406 Compare August 20, 2026 04:16
@github-actions

Copy link
Copy Markdown

LLGo baseline benchmarks

335c4060da28 | 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% 264.030 ms +1.508 ms / +0.6% (worse) 1.140 ms +77.4 us / +7.3% (worse)
Linux fmtprintf 1860144 B -8 B / -0.0004301% (better) 2.059 s -7.402 ms / -0.4% (better) 2.803 ms -1.85 us / -0.1% (better)
Linux println 68776 B 0 B / +0.0% 263.287 ms +4.991 ms / +1.9% (worse) 1.353 ms -1.279 us / -0.1% (better)
macOS cprintf 84672 B 0 B / +0.0% 607.776 ms +42.69 ms / +7.6% (worse) 3.573 ms +205.5 us / +6.1% (worse)
macOS fmtprintf 1892720 B 0 B / +0.0% 2.864 s -1.432 s / -33.3% (better) 15.347 ms -1.514 ms / -9.0% (better)
macOS println 121360 B 0 B / +0.0% 375.177 ms -110.1 ms / -22.7% (better) 3.408 ms -303.3 us / -8.2% (better)
Core language and compiler benchmarks
Platform Benchmark ns/op vs base
Linux BenchmarkLookupPCRandom 9.640 ns/op +0.097 ns/op / +1.0% (worse)
Linux BenchmarkMergeCompilerFlags 112 ns/op +0.1 ns/op / +0.1% (worse)
Linux BenchmarkMergeLinkerFlags 72.950 ns/op +0.12 ns/op / +0.2% (worse)
Linux BenchmarkChannelBuffered 28.120 ns/op -0.02 ns/op / -0.1% (better)
Linux BenchmarkChannelHandoff 19777 ns/op +338 ns/op / +1.7% (worse)
Linux BenchmarkDefer 34.900 ns/op +0.16 ns/op / +0.5% (worse)
Linux BenchmarkDirectCall 1.363 ns/op 0 ns/op / +0.0%
Linux BenchmarkGlobalRead 1.363 ns/op 0 ns/op / +0.0%
Linux BenchmarkGlobalWrite 2.179 ns/op -0.001 ns/op / -0.04587% (better)
Linux BenchmarkGoroutine 24713 ns/op -713 ns/op / -2.8% (better)
Linux BenchmarkInterfaceCall 7.094 ns/op +0.004 ns/op / +0.1% (worse)
Linux BenchmarkRuntimeGetG 1.637 ns/op -0.001 ns/op / -0.1% (better)
macOS BenchmarkLookupPCRandom 15.480 ns/op -5.29 ns/op / -25.5% (better)
macOS BenchmarkMergeCompilerFlags 179.400 ns/op -26.5 ns/op / -12.9% (better)
macOS BenchmarkMergeLinkerFlags 99.920 ns/op -39.18 ns/op / -28.2% (better)
macOS BenchmarkChannelBuffered 23.570 ns/op +0.86 ns/op / +3.8% (worse)
macOS BenchmarkChannelHandoff 9203 ns/op +885 ns/op / +10.6% (worse)
macOS BenchmarkDefer 30.540 ns/op -0.76 ns/op / -2.4% (better)
macOS BenchmarkDirectCall 1.083 ns/op +0.005 ns/op / +0.5% (worse)
macOS BenchmarkGlobalRead 1.077 ns/op -0.092 ns/op / -7.9% (better)
macOS BenchmarkGlobalWrite 1.254 ns/op +0.143 ns/op / +12.9% (worse)
macOS BenchmarkGoroutine 29732 ns/op -7805 ns/op / -20.8% (better)
macOS BenchmarkInterfaceCall 4.541 ns/op -1.036 ns/op / -18.6% (better)
macOS BenchmarkRuntimeGetG 2.113 ns/op -0.082 ns/op / -3.7% (better)

Compared with bed9d7c4d4c0 measured in the same runner job.

@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 63.02521% with 44 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
cl/static_init.go 63.47% 30 Missing and 12 partials ⚠️
ssa/decl.go 50.00% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@cpunion

cpunion commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator Author

Closing in favor of #2371 which consolidates the dependency bump, loader refactoring, and static-init composite literal folding optimization into a single cohesive PR.

@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