Skip to content

Fix O(N^2) tail-call trampoline teardown under GCC/MSVC (fixes #50) - #51

Merged
revarbat merged 1 commit into
mainfrom
fix-tco-quadratic-trampoline
Jul 30, 2026
Merged

Fix O(N^2) tail-call trampoline teardown under GCC/MSVC (fixes #50)#51
revarbat merged 1 commit into
mainfrom
fix-tco-quadratic-trampoline

Conversation

@revarbat

Copy link
Copy Markdown
Member

Summary

  • Root cause of O(N^2) interpreter tail-call trampoline under GCC/libstdc++ (not Clang/libc++) -- 1000x CI slowdown #50: both tail-call trampolines keep every hop's EvalContext alive in a chain vector for the whole run, then relied on std::vector<EvalContext>'s own destructor to tear it down. That destruction order is unspecified by the C++ standard -- libc++ (Clang) destroys back-to-front, matching ScopeTrailStorage::popLevel's "scan from the back" optimization; libstdc++ (GCC) destroys front-to-back, the adversarial order for that same scan, turning an intended O(N) teardown into a real O(N^2).
  • Confirmed with a minimal repro (std::vector<T>::clear() prints opposite destruction order under clang++ vs g++-16 for identical source) and an N-scaling experiment (Clang: clean O(N); GCC: time quadruples when N doubles).
  • Fix: an RAII guard tears chain down back-to-front explicitly, on every exit path (normal return and exception unwind -- the recursion-guard error these tests hit is exactly the case a trailing-statement teardown would have missed).
  • Re-enables the two tests skipped in Compile functions that create escaping closures (Op::MakeClosure) #49's merge (TailCalls.InfiniteTailRecursionHitsTheIterationCapInsteadOfHanging, TailCalls.InfiniteTailRecursionErrorMentionsTheFunctionName), plus adds a timing-bounded regression tripwire so a future regression fails fast instead of silently reintroducing a 50-minute CI run.

Test plan

  • Both previously-skipped tests: 25-50+ min each on GCC -> under 1s, matching Clang, verified locally with a real Homebrew GCC 16 build on the same machine/OS/allocator as the Clang baseline.
  • Full suite (1292 tests): 100% passing on both Clang and GCC locally.
  • New TailCalls.InfiniteTailRecursionHitsTheCapInBoundedWallTime regression test (generous 30s ceiling, won't trip on ordinary machine noise).

🤖 Generated with Claude Code

Both trampolines (evalFunctionBodyTrampoline, user_calls.cpp; and
runCompiledFunctionTrampoline/runCompiledFunctionFromBoundTrampoline,
bytecode_vm.cpp) keep every hop's own EvalContext alive in a `chain`
vector for the whole run ($-vars stay dynamically scoped through even
an isolated call, so a later hop may need to walk back through an
earlier one's dyn level). Tearing `chain` down was left to its own
std::vector<EvalContext> destructor -- whose element-destruction order
is unspecified by the C++ standard. libc++ (Clang) destroys
back-to-front, matching ScopeTrailStorage::popLevel's "scan from the
back" optimization (its own doc comment assumes the level being popped
is usually the most recently pushed one); libstdc++ (GCC) destroys
front-to-back, the adversarial order for that same scan -- each pop
then walks almost the entire remaining vector, turning an intended
O(N) teardown into a real, measured O(N^2).

Confirmed via a minimal repro (std::vector<T>::clear() literally
prints destruction order in opposite directions under clang++ vs
g++-16 for identical source) and an N-scaling experiment (Clang: clean
O(N); GCC: time roughly quadruples when N doubles, at every step).
The two previously-skipped tests (see #50) went from 25-50+ minutes
each on GCC to under 1 second, matching Clang throughout -- verified
locally with a real Homebrew GCC 16 build (same machine/OS/allocator
as the Clang baseline) and against the full 1292-test suite on both
compilers.

Fix: an RAII guard tears `chain` down back-to-front explicitly, on
every exit path (normal return OR exception unwind -- the recursion-
guard error these tests hit is exactly the case a trailing-statement
teardown would miss). Re-enables the two tests previously skipped on
non-Apple CI, plus a new timing-bounded regression tripwire.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@revarbat
revarbat merged commit 2eb0ef6 into main Jul 30, 2026
3 checks passed
@revarbat
revarbat deleted the fix-tco-quadratic-trampoline branch July 30, 2026 06:41
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