Reuse hoisted function definitions across re-evaluations on an engine#2613
Merged
Conversation
Declaration instantiation constructed a fresh JintFunctionDefinition for every hoisted function on every evaluation - and the definition owns the lazily-built body handler tree, so re-running a prepared script rebuilt each called function's interpreter subtree and reset every per-node inline cache. Function-level and block-level DI paid the same cost per call for inner declarations. Definitions are now cached per engine, keyed on the declaration AST node: - a strong engine-owned Dictionary, NOT the AST's shared UserData State (handler trees accumulate engine-affine cache entries; sharing them across engines retains dead engines - the SharedPreparedScriptDoesNotRetainEngines tripwire) and NOT a ConditionalWeakTable (a dependent handle keeps its value alive on KEY reachability alone, so with the key rooted by a live Prepared<Script> the value->inline caches->engine chain pins the dropped engine) - the global-DI site engages the cache only on RE-evaluation of a script this engine has seen (one HashSet probe), so fresh-engine-per-run hosts execute the exact uncached path; block/function-level DI sites cache unconditionally since repeated calls within a single evaluation already amortize them - a 2048-entry reset backstops hosts streaming endless distinct sources through one engine Measurements (default job; the whole window drifted uniformly +5-8%, so in-window ratios and the deterministic allocation columns carry the signal): - new PreparedAnomalyBenchmarks.PreparedReusedEngine (the cached- Prepared embedding pattern): allocation 14.61 -> 9.99 KB/op (-32%), in-window time ratio 1.10x -> 1.08x vs SourcePerOp - diagnostic probe (small function-declaring script, engine reuse): 3,288 -> 1,740 B/op (-47%) - every fresh-engine row allocation-byte-identical (SourcePerOp / PreparedShared / PreparedPerOp / LinqJs / EvalExecution) Full Jint.Tests green including all GarbageCollectionTests retention pins on both TFMs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 10, 2026
lahma
added a commit
that referenced
this pull request
Jul 10, 2026
… instance (#2616) Per-engine definition reuse (#2613) shares one handler tree across the fresh function instances a re-evaluated prepared script creates. SlotLocationCache cached the first instance's environment and returned a permanent miss once that environment became unreachable, silently disabling every slot-backed fast lane - unboxed loop counters, comparison/equality lanes, sum-of-products - from the second re-evaluation onward (a JsNumber materialization per identifier read in tight loops). On an unreachable cached environment, fall through and re-resolve against the current chain exactly like the cross-engine Prepared<Script> path; one bounded walk per instance switch, subsequent reads hit the re-populated cache. Claude-Session: https://claude.ai/code/session_01BY8HoKam5H8vTPn1bMY2Hv Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
lahma
added a commit
that referenced
this pull request
Jul 10, 2026
* Re-resolve slot locations when a shared handler tree changes function instance Per-engine definition reuse (#2613) shares one handler tree across the fresh function instances a re-evaluated prepared script creates. SlotLocationCache cached the first instance's environment and returned a permanent miss once that environment became unreachable, silently disabling every slot-backed fast lane - unboxed loop counters, comparison/equality lanes, sum-of-products - from the second re-evaluation onward (a JsNumber materialization per identifier read in tight loops). On an unreachable cached environment, fall through and re-resolve against the current chain exactly like the cross-engine Prepared<Script> path; one bounded walk per instance switch, subsequent reads hit the re-populated cache. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BY8HoKam5H8vTPn1bMY2Hv * Add member-bound loop test lane for i < arr.length / i < s.length The comparison/equality lane gains a third right-operand shape: a non-computed, non-optional .length read off a slot-resolved identifier base. JsArray bases read the own length data property (accessors impossible) and JsString bases read the virtual Length (no materialization); everything else - typed arrays, proxies, arguments, plain objects with getters - declines to the generic path before any side effect. The length is re-read live every iteration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BY8HoKam5H8vTPn1bMY2Hv --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
lahma
added a commit
that referenced
this pull request
Jul 10, 2026
Full re-run after the 2026-07-10 batch (#2613, #2615-#2620). Highlights vs the 2026-07-09 refresh: dromaeo-string-base64 allocation -30% (2.30 -> 1.62 MB/op) and -modern -4.3% time, the classic row now ranked first outright (previously tied with NiL.JS) - loop tests of the form i < s.length read the length unboxed through the comparison lane (#2617). The batch's engine-reuse wins (#2613/#2615 definition reuse, #2616 slot-cache lane restoration) target long-lived-engine embeddings and are not visible in this fresh-engine-per-op table. Everything else moved within documented noise bands; competitor rows served as thermal canaries (times within +/-3%, allocations byte-identical). Claude-Session: https://claude.ai/code/session_01BY8HoKam5H8vTPn1bMY2Hv Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.
What
Declaration instantiation constructed a fresh
JintFunctionDefinitionfor every hoisted function onevery evaluation — and the definition owns the lazily-built body handler tree, so re-running a
prepared script rebuilt each called function's interpreter subtree and reset every per-node inline
cache. Function-level and block-level DI paid the same cost per call for inner declarations.
Definitions are now cached per engine, keyed on the declaration AST node.
Why this exact shape (two rejected designs)
State: handler trees accumulate engine-affine cache entries,and sharing them across engines retains dead engines —
GarbageCollectionTests.SharedPreparedScriptDoesNotRetainEnginesfailed immediately.ConditionalWeakTable: a dependent handle keeps its value alive on key reachabilityalone, so with the key rooted by a live
Prepared<Script>, the value → inline caches → enginechain pins the dropped engine — the same test failed again, in the opposite direction of the
naive mental model.
Dictionary(dies with the engine; retention scopedto engine lifetime, mirroring
Realm._templateMap), with the global-DI site engaging the cacheonly on re-evaluation of a script this engine has seen (one
HashSetprobe) — sofresh-engine-per-run hosts execute the exact uncached path. Block/function-level DI sites cache
unconditionally since repeated calls within one evaluation already amortize them. A 2048-entry
reset backstops hosts streaming endless distinct sources through one engine.
Results
Default job; the measurement window drifted uniformly +5–8% across all rows (including untouched
guards), so in-window ratios and the deterministic allocation columns carry the signal:
Prepared<Script>)Gating
All-TFM build ✅ · Jint.Tests ×2 ✅ (including all GC retention pins) · PublicInterface ×2 ✅ ·
Test262 99,426/0 ✅
🤖 Generated with Claude Code