perf26: for-in by prototype chain, eval-scope bypass, helper register alignment - #159
Merged
Merged
Conversation
A `for-in` site cached its key list per target object, so a loop over many objects -- `walk(k, v)` recursing through a parsed JSON tree -- rebuilt it every time: each layer's keys hashed into a shadowing set, Object.prototype's non-enumerable ones included. The site now also remembers the prototype chain of the last ordinary target, at its layout revisions, with the keys that chain contributes; an object inheriting exactly that chain enumerates its own enumerable keys and then those of the chain it does not shadow. The wide tier's `for-in` exits answer ordinary objects without building an environment, and a computed `array["length"]` skips key conversion. A two-key `for-in` went from 3599 to 1346 cycles (QuickJS-NG 1082); string-tagcloud 0.965. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The typed loop's f64 helper executor fills its register file on every call, and where the caller's frames left the stack decided how many instructions the fill took: recursive_call_tree executed 0.9% more instructions after an unrelated change resized a frame above it. The file is now its own 64-byte-aligned type, which makes the count independent of the callers (0.998 against main). `NumProgram::run` is pinned in the hot-function order next to the wide tier's numeric executor. The sentinel still runs about 10% more cycles than on main with byte-identical helper code at the same offset: its read-only jump tables moved with the constant data of unrelated code, which the order file cannot place. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A function with a direct `eval` gives every closure it creates its dynamic
scope, so each call of one built a full interpreter frame even when the
closure resolves none of the scope's names -- string-tagcloud's JSON `walk`
and date-format-tofte's formatters. Such a closure now keeps the scope but
bypasses it: the direct call path and the compact tiers take it as scope
free, and the scope records it and revokes the bypass, with every fact
derived from it, the moment it adds, removes or remaps a name -- a later
`eval('var String = 5')` in that function is then visible to the closure.
Frames that can suspend keep the scope, and a scope lets at most 64
closures bypass it at a time. The rule for closures made by direct-eval
code itself (which drop the scope outright) is now the same decision.
string-tagcloud 0.784, date-format-tofte 0.979.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The task file passed its 600-line limit; perf8 through perf20's stack runs and measurements move to tasks/archive/T033-screen-log-early.md unchanged, with a pointer in the screen log. Co-Authored-By: Claude Opus 5.5 (1M context) <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.
for-inover many objects of one shape: each site now remembers the prototype chain of its last ordinary target, together with the keys that chain contributes. An object with the same chain then enumerates its own keys plus any inherited keys it doesn't shadow, without the per-layer hash set. The wide tier'sfor-inexits handle ordinary objects without building an environment, and a computedarray["length"]read skips key conversion. A two-keyfor-inwent from 3,599 to 1,346 cycles (NG 1,082).eval: they bypass that function's dynamic scope as long as the scope binds none of the names they resolve by name. The scope keeps a list of these closures and revokes every bypass, together with the memoized call-path facts, the next time it adds, removes or remaps a name. Frames that can suspend are excluded, and a scope allows at most 64 bypassing closures. string-tagcloud's JSONwalkand tofte's formatters now take the direct call path.NumProgram::runis pinned in the hot-function order.Formal run fe533cc against main cebad5e, 30 blocks, cycles (
target/comparison/perf26-fe533cc7-30b):Test262: the gap scans show 0 actionable gaps against QuickJS-NG for these slices:
I added unit tests for bypass revocation (a later
eval('var String = 5'), generators, the 64-closure limit) and for prototype keys appearing and disappearing mid-run.🤖 Generated with Claude Code