Skip to content

perf26: for-in by prototype chain, eval-scope bypass, helper register alignment - #159

Merged
Lewin671 merged 5 commits into
mainfrom
agent/perf-25/claude
Sep 26, 2026
Merged

Lewin671 merged 5 commits into
mainfrom
agent/perf-25/claude

Conversation

@Lewin671

Copy link
Copy Markdown
Owner
  • for-in over 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's for-in exits handle ordinary objects without building an environment, and a computed array["length"] read skips key conversion. A two-key for-in went from 3,599 to 1,346 cycles (NG 1,082).
  • Closures created by a function that contains a direct 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 JSON walk and tofte's formatters now take the direct call path.
  • Numeric helper registers: the register file is 64-byte aligned, so its instruction count no longer depends on caller frame sizes. NumProgram::run is pinned in the hot-function order.

Formal run fe533cc against main cebad5e, 30 blocks, cycles (target/comparison/perf26-fe533cc7-30b):

  • External geomean was 0.995 against main and 0.749 against QuickJS-NG.
  • string-tagcloud went to 0.759 against main (1.31 → 1.000 against NG), and date-format-tofte to 0.975.
  • Sentinels ranged from 0.993 to 1.002.
  • JetStream gaussian-blur measured 1.057. The same binaries measure 1.001 by min-of-5 alternation, which matches the path sensitivity already recorded in T033.

Test262: the gap scans show 0 actionable gaps against QuickJS-NG for these slices:

  • language/eval-code, annexB eval-code, built-ins/eval
  • language/function-code, arguments-object
  • statements/with, statements/for-in, statements/generators
  • expressions/async-function
  • built-ins/Object/keys

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

Lewin671 and others added 5 commits September 26, 2026 06:23
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>
@Lewin671
Lewin671 merged commit 7a83b56 into main Sep 26, 2026
6 of 10 checks passed
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