Skip to content

fix: run the backend at -Oless for unoptimized builds - #1821

Open
ghaith wants to merge 4 commits into
release/1.0.xfrom
backport/1.0.x/onone-stack-coloring
Open

fix: run the backend at -Oless for unoptimized builds#1821
ghaith wants to merge 4 commits into
release/1.0.xfrom
backport/1.0.x/onone-stack-coloring

Conversation

@ghaith

@ghaith ghaith commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Companion to #1819 for release/1.0.x (stacked on it; retargets automatically when #1819 merges). Together they close the string-temp stack-exhaustion problem for unoptimized builds.

Problem

#1819 brackets lowered call temporaries with lifetime markers so LLVM's stack coloring can overlap their slots — but stack coloring is a machine pass that only runs when the backend optimizes. At -Onone the markers are inert, and string-heavy POU bodies keep one dead STRING[2048] slot per call site: a representative function block needs ~112 KB of frame and overflows constrained task stacks at entry.

Fix

Decouple the two optimization dials. -Onone keeps the IR pipeline at default<O0> — no inlining, no unrolling, no IR-level transforms that damage source-level debugging — but creates the TargetMachine at codegen level Less, so the machine pipeline (instruction selection via SelectionDAG, greedy register allocation, and crucially stack coloring) runs.

Frame size of the representative function block at -Onone: 111,760 B → 10,312 B.

Debug-experience validation

Compared gdb behavior on a multi-file test program (small inlinable functions, cross-file calls, unrollable and data-dependent loops, string-taking and string-returning functions) at -Onone before/after, plus -Oless/-Odefault/-Oaggressive for context:

  • Function and line breakpoints bind and hit identically; loop-body line breakpoints still hit once per iteration with the induction variable readable at each stop.
  • Backtraces, info args/info locals, and finish behave identically; zero <optimized out> locals (those appear starting at -Oless, caused by the IR pipeline, which stays off here).
  • Line-stepping through loops actually improves: the FastISel-era quirk where next repeated the loop-header line and never showed the body is gone with SelectionDAG.
  • One artifact: reading a local before its first assignment can show stack garbage where it previously happened to read the zero-initialized slot. Initialized values are unaffected.

Testing

Full unit suite, snapshot tests (no drift) and the lit end-to-end suite pass. Frame measurement above from objdump on the compiled object.

🤖 Generated with Claude Code

@ghaith
ghaith requested a review from mhasel July 21, 2026 09:08
ghaith and others added 2 commits July 21, 2026 13:40
Every call to an aggregate-returning function allocates a caller-side
temporary sized to the declared return type (STRING[2048] for the
stdlib string functions). These allocas carried no lifetime
information, so LLVM kept one dead slot per call site alive for the
whole function: string-heavy POU bodies easily exceed 100 KB of frame
and overflow constrained runtime task stacks.

Bracket each lowered statement's temporaries with
llvm.lifetime.start/end so LLVM's stack coloring can overlap slots of
consecutive statements; a string-heavy test body shrinks from ~111 KB
to ~10 KB of frame at the default optimization level. Unoptimized
builds are unchanged: stack coloring is a machine pass that only runs
when the backend optimizes.

Temps whose address outlives their statement are pinned instead
(Allocation::statement_scoped = false) and get no markers: ADR/REF
arguments, REF= right-hand sides, and interface fat-pointer captures
(the polymorphism lowering wraps those in ADR before this pass runs).
Loop bookkeeping temporaries keep their function-long storage for the
same reason.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
-Onone keeps the IR pipeline at O0 — no inlining, unrolling or other
debug-affecting IR transforms — but now creates the TargetMachine at
codegen level Less so LLVM's machine pipeline runs. Most importantly
this enables stack coloring, which merges the lifetime-bracketed call
temporaries: a string-heavy function block drops from ~112 KB to
~10 KB of frame at -Onone, fitting constrained runtime task stacks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ghaith
ghaith force-pushed the backport/1.0.x/onone-stack-coloring branch from 9fab2f7 to 0e3419d Compare July 21, 2026 11:48
@ghaith
ghaith force-pushed the backport/1.0.x/lifetime-markers branch from 8a51916 to 91fe41e Compare July 21, 2026 11:48
ghaith and others added 2 commits July 27, 2026 09:49
Combine the lifetime-marker pinning with the deferred-reference handling from
#1825 the same way the master branch (#1803) does.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…one-stack-coloring

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Base automatically changed from backport/1.0.x/lifetime-markers to release/1.0.x July 29, 2026 08:29
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