Repository navigation
lazy: bound the collection budget by the arena - #68
Merged
olydis merged 1 commit intoOct 6, 2026
Merged
Conversation
Three paths let the lazy evaluator's _head pass ARENA_NODES = 2^30, so a node is written past the 8 GiB reservation and an index gets the MARK bit: the budget doubled with no ceiling (a live set over 2^29 nodes), set_budget took any size (RUNNER_RSS_THRESHOLD_MB over 8192), and a step allocates two nodes after a check that left room for one (a budget of the whole arena: RUNNER_RSS_THRESHOLD_MB=0 or 8192). As the eager evaluator does: the budget stops at the arena, and a live set that needs more throws "arena exhausted: the live set does not fit". On top, for the lazy step's second node, the check leaves room for two; set_budget clamps; and whnf drops what a failed reduction left on the spine and the roots, so its redexes stop being roots. It does that around the recursion, now force(), rather than in it: a handler in every frame took clang's from 72 to 104 bytes, 29% less depth on the same stack. `grow grow △` with grow = \v \acc v v (△ acc acc), lazy runner, default budget: SIGSEGV after 77 s (lambada blocks on the dead runner) before; "arena exhausted: the live set does not fit" after 74 s, arena 2^30 - 2 nodes, and the next request answered, after. test-lazy.cpp runs the evaluator on a 2^12-node arena (ARENA_BITS, a template parameter for that): it fails on the old code, and with any one of the ceiling, the room for two, the clamp, or either reset taken out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ffv5AybPTpV56D5vuayQr
olydis
force-pushed
the
claude/tc-performance-optimizations-rqtyzl-lazy-budget-ceiling
branch
from
October 4, 2026 22:38
325d90e to
5658332
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bug
In the lazy evaluator (
lazy-graph-nil-mmap-32.hpp, the runner's default),_headcan go pastARENA_NODES = 2^30: a node is written past the 8 GiB reservation, and its index (≥ 2^30) has the MARK bit set. Three ways there:while (_live * 2 > _budget) _budget *= 2;has no ceilingset_budgetaccepts any sizeRUNNER_RSS_THRESHOLD_MB> 8192_free || _head < _budget)RUNNER_RSS_THRESHOLD_MB=0or8192, or one that doubled up to itRepro: real 2^30 arena, default budget, a term whose live set grows without bound (
grow grow △,grow = \v \acc v v (△ acc acc)), sent to the lazy runner followed by a small request:timeout(rc=124).err arena exhausted: the live set does not fitafter 74 s, arena at 1,073,741,822 nodes (2^30 − 2), and the next request is answered. Through lambada:arena.lamb:3: runner: arena exhausted: the live set does not fit, rc=1, after 73 s.Fix
arena exhausted: the live set does not fit.collect_if_over_budgetreturns early only if two nodes fit ((_free && _arena[_free].v) || _head + 2 <= _budget), so a step never takes_headpast the budget.set_budgetclamps to the arena.whnfrestores the spine and the root stack when an exception passes (eager's_stack.resize(base)); otherwise the failed request's redexes stay roots and every later request fails too. The handler sits inwhnf, around the recursion (now the privateforce), not in every recursive frame: there it took clang's frame from 72 to 104 bytes, i.e. 29% less forcing depth on the same stack (gcc: 160 either way).BasicLazyGraphNilMmap32<ARENA_BITS = 30>;LazyGraphNilMmap32is an alias, so callers do not change.runner.md: one sentence on the ceiling.Test
dag-machine/test-lazy.cpp(built and run bytest.sh, next totest-jets.cpp) runs the evaluator on a 2^12-node arena: a budget past the arena still collects at its end; a live set that does not fit throws, with_headinside the arena; afterwards it reduces correctly again._headpast the arena; SIGSEGV instead when nothing is mapped right after it)Validation
dag-machine/test.shpasses underLC_ALL=CandLC_ALL=C.UTF-8,test-runner.sh21/21.test-lazy.cppandrunner.cppbuild with g++ 13 and clang++ under-Wall -Wextrawith no new warnings.test.cpp's lazy sanity and collection checks pass.whnfof(K y) △nested n deep): g++ 51.7k before and after; clang++ 104k before and after (74k with the handler in the recursion).--jobs 4, cold cache,RUNNER_WORKER_STACK_MB=4096), main → this branch: 561.2 s → 569.6 s (+1.5%, one run each),srcunchanged by either. The new check is +1.4% instructions on recursive fib (cachegrind, g++); timing on that micro-benchmark is within ±5% either way across compilers.Repro: the runner gets
load <one-line module>,reduce dag <grow>, then a smallreduce. lambada:TREE_CALCULUS_RUNNER=1 LAMBADA_TREE_CALCULUS=<this checkout> node submodules/lambada/bin/lambada.js expect-test bundle.dag --root src, withsrc/arena.lambholding the two lines above.Caveats
stem/fork/applyfrom the runner's parser orbindcan still pass the arena once the budget is the whole arena and a payload needs more nodes than the free list has left. Eager has the same gap inintern. A throw inallocwould not help: nothing collects before the next request parses, so every later request would fail.set_budgetdoes not clamp on main (RUNNER_RSS_THRESHOLD_MB> 16384); eager: fail a request that would pass RUNNER_RSS_LIMIT_MB #67 clamps it.certify/sizetests, hence 4 GiB above; after a failed test the runner outlives lambada (runner: one-way FIFO ends, so neither side outlives the other #66).🤖 Generated with Claude Code
https://claude.ai/code/session_018ffv5AybPTpV56D5vuayQr