Skip to content

ltoplugin: preserve names for shared sret slots - #2345

Open
zhouguangyuan0718 wants to merge 1 commit into
xgo-dev:mainfrom
zhouguangyuan0718:codex/fix-lto-shared-sret
Open

ltoplugin: preserve names for shared sret slots#2345
zhouguangyuan0718 wants to merge 1 commit into
xgo-dev:mainfrom
zhouguangyuan0718:codex/fix-lto-shared-sret

Conversation

@zhouguangyuan0718

Copy link
Copy Markdown
Contributor

Summary

  • group annotated MethodByName calls that reuse the same sret storage
  • refine their checked loads with the union of all proven method names
  • retain generic markers when any shared call has an unknown name
  • add a focused LLVM IR/FileCheck regression for known and unknown shared-sret cases

Motivation

After PR #2344 changes bounds-check CFG shape, LLVM fully unrolls the range-literal fixture into three MethodByName calls that reuse one sret alloca. The plugin previously walked every checked load reachable from that alloca while processing the first call, so all three loads were refined to Query. GlobalDCE then incorrectly removed Mutation and Subscription.

This change keeps the refinement fail-closed while allowing known shared calls to retain only the union of their reachable method names.

Validation

  • the new FileCheck fixture fails with the old plugin because every load is marked Query
  • Ubuntu LLVM 19.1.7: full existing LTO-plugin cl test selection passes
  • Ubuntu LLVM 19.1.7: PR ssa: isolate index range panic path #2344 range-literal symbol test passes with the fixed plugin
  • the PR ssa: isolate index range panic path #2344 reproduction retains Query, Mutation, and Subscription, drops Drop, and runs with the expected three-line output
  • go test ./internal/lto
  • go test ./cl -run ^TestBuildAndCheckSymbolsFromTestltoLTOPluginSharedSRet$ -count=1

@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: fix LTO plugin shared sret refinement

Solid, well-targeted fix. The core correctness invariant holds: when multiple MethodByName calls share one sret slot, the pass now groups them (via ProcessedCalls + shared-storage matching) and refines the shared checked-loads with the union of every provably-known name, rather than letting the first call claim all of them. The all-or-nothing KnownNames fallback is preserved — if any grouped call has an unprovable name, all generic markers are left intact so GlobalDCE stays conservative. Memory safety, iterator validity across the IR edits, and recursion-depth bounds all check out. The @shared_sret / @shared_sret_unknown FileCheck cases exercise both the refine and the conservative-fallback paths.

A few non-blocking notes are inline. None are correctness bugs.

Suggestions (no change required):

  • Consider a test case where two grouped calls request the same constant name, with a CHECK-COUNT-1 assertion, to lock in the addName dedup invariant that the union relies on (the current test uses three distinct names).

Value *SRetStorage = getSRetStorage(ReflectCall);
SmallVector<CallBase *, 4> SharedCalls{ReflectCall};
if (SRetStorage) {
for (CallBase *Candidate : Calls) {

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.

Minor (perf): this inner scan re-walks the entire Calls vector for every unprocessed ReflectCall, and getSRetStorage(Candidate) re-runs stripPointerCasts() each time — so the grouping is O(n^2) over the number of MethodByName call sites, with repeated pointer-cast walks. Correctness is fine (the ProcessedCalls set prevents reprocessing), and n is small in practice since Calls only holds attribute-tagged reflect calls. If you want to harden against large modules, a single pre-pass building DenseMap<Value* /*sret storage*/, SmallVector<CallBase*>> collapses this to O(n) and computes each storage once. Not required.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed on the complexity tradeoff. I kept the small scan because this vector only contains attribute-tagged reflect calls and is expected to stay small; a DenseMap grouping pass would be a reasonable follow-up if large-module profiles show this matters.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Follow-up: I implemented the suggested pre-grouping with DenseMap after all. Same-machine full plugin coverage was 198.6s with pre-grouping versus about 200.3s before, so the long CI success path was mostly later demos rather than a measured regression, but this removes the O(n^2) scan and computes each cast-stripped storage once.

bool KnownNames = collectStringSetFromCallArgs(ReflectCall, DL, Names);
bool KnownNames = true;
for (CallBase *SharedCall : SharedCalls) {
if (!collectStringSetFromCallArgs(SharedCall, DL, Names)) {

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.

Behavior note: MaxReflectMethodNames (32) is now applied to the union of names across the whole shared group, not per call. A shared slot with several calls whose combined unique names exceed 32 will hit the cap in addName, set KnownNames=false, and refine none of them (falling back to the generic marker). This is safe/conservative, but the shared budget is a subtle change from the previous per-call limit — worth a one-line comment here so a future maintainer isn't surprised.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a comment that the bounded name budget applies to the combined shared group and that exceeding it preserves the generic markers.

continue;
}

// Optimizations can unroll several MethodByName calls while reusing one

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.

Doc nit: "Optimizations can unroll several MethodByName calls" attributes the shared slot specifically to loop unrolling, but the code actually groups any distinct calls whose getSRetStorage result is pointer-identical (slot reuse / stack coloring / etc.) — the test itself uses three independent source-level calls, not an unrolled loop. Consider softening to something like "Optimizations can leave several MethodByName calls sharing one sret slot". Also note the "sees the loads for all of those calls" guarantee relies on the exact getSRetStorage(Candidate) == SRetStorage equality at line 1329 (cast-stripped identity, not aliasing) — fine today, just an implicit invariant.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated the wording to cover any optimization that leaves calls sharing pointer-identical, cast-stripped sret storage rather than attributing it specifically to loop unrolling.

@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown

LLGo baseline benchmarks

ec265994c38e | workflow run | long-term charts

Program measurements

Platform Workload File size vs base Build vs base Run vs base
Linux cprintf 19248 B +0.0% 352.761 ms +8.0% (worse) 1.279 ms -6.4% (better)
Linux fmtprintf 1879904 B +0.0% 2.654 s +0.9% (worse) 3.240 ms +0.8% (worse)
Linux println 68616 B +0.0% 326.270 ms +2.3% (worse) 1.587 ms +1.6% (worse)
macOS cprintf 84624 B +0.0% 337.309 ms -11.6% (better) 2.484 ms -31.4% (better)
macOS fmtprintf 1891424 B +0.0% 2.632 s -4.0% (better) 14.287 ms +16.0% (worse)
macOS println 121168 B +0.0% 354.768 ms -8.8% (better) 3.087 ms -5.2% (better)
Core language and compiler benchmarks
Platform Benchmark ns/op vs base
Linux BenchmarkLookupPCRandom 13.280 ns/op +0.3% (worse)
Linux BenchmarkMergeCompilerFlags 151.600 ns/op +0.3% (worse)
Linux BenchmarkMergeLinkerFlags 94.500 ns/op -0.1% (better)
Linux BenchmarkChannelBuffered 35.270 ns/op +0.1% (worse)
Linux BenchmarkChannelHandoff 28245 ns/op +6.0% (worse)
Linux BenchmarkDefer 44.640 ns/op -0.7% (better)
Linux BenchmarkDirectCall 1.556 ns/op +0.1% (worse)
Linux BenchmarkGlobalRead 1.557 ns/op +0.1% (worse)
Linux BenchmarkGlobalWrite 2.479 ns/op +0.0%
Linux BenchmarkGoroutine 41776 ns/op +0.3% (worse)
Linux BenchmarkInterfaceCall 8.096 ns/op +0.1% (worse)
Linux BenchmarkRuntimeGetG 2.181 ns/op +0.0%
macOS BenchmarkLookupPCRandom 11.950 ns/op -9.4% (better)
macOS BenchmarkMergeCompilerFlags 112.700 ns/op -16.3% (better)
macOS BenchmarkMergeLinkerFlags 75.010 ns/op -2.8% (better)
macOS BenchmarkChannelBuffered 28.200 ns/op +33.5% (worse)
macOS BenchmarkChannelHandoff 13328 ns/op +83.5% (worse)
macOS BenchmarkDefer 27.870 ns/op +1.9% (worse)
macOS BenchmarkDirectCall 1.057 ns/op +0.6% (worse)
macOS BenchmarkGlobalRead 1.091 ns/op -8.2% (better)
macOS BenchmarkGlobalWrite 1.117 ns/op +5.3% (worse)
macOS BenchmarkGoroutine 26831 ns/op +2.2% (worse)
macOS BenchmarkInterfaceCall 5.989 ns/op +4.4% (worse)
macOS BenchmarkRuntimeGetG 2.614 ns/op +1.3% (worse)

Compared with 7f954d6182d1 measured in the same runner job.

@zhouguangyuan0718
zhouguangyuan0718 force-pushed the codex/fix-lto-shared-sret branch from 75070fb to f908b33 Compare August 17, 2026 06:14
@zhouguangyuan0718

Copy link
Copy Markdown
Contributor Author

Addressed the non-blocking review suggestions in f908b332d: clarified the shared-group name budget and generalized the slot-sharing comment, and added a shared_sret_duplicate FileCheck case that asserts duplicate names do not create duplicate markers. Kept the small call-vector scan rather than introducing a DenseMap; rationale is in the inline reply.

@zhouguangyuan0718
zhouguangyuan0718 force-pushed the codex/fix-lto-shared-sret branch from f908b33 to ec26599 Compare August 17, 2026 06:38
@codecov

codecov Bot commented Aug 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

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