Skip to content

Route member-expression identifier-object reads through identifier caches#2660

Merged
lahma merged 1 commit into
sebastienros:mainfrom
lahma:perf-member-ident-read
Jul 13, 2026
Merged

Route member-expression identifier-object reads through identifier caches#2660
lahma merged 1 commit into
sebastienros:mainfrom
lahma:perf-member-ident-read

Conversation

@lahma

@lahma lahma commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

JintMemberExpression.EvaluateInternal resolves an identifier base (a in a[i] = v, a[i] += x, a[i]++, obj[m](), delete obj.prop) by calling JintEnvironment.TryGetIdentifierEnvironmentWithBindingValue directly — a full environment-chain walk with virtual TryGetBinding probes on every evaluation. That call predates the slot/global/nested-chain caches that JintIdentifierExpression.GetValue has since grown; the read paths (GetValue, GetCalleeForCall, TryAssignFast) were upgraded to those caches long ago, this branch never was.

This PR routes the identifier-object read through identifierExpression.GetValue(context) so it hits the identifier node's own caches (hop-0 slot reads, validated global descriptors), with one carve-out: arguments / eval bases keep the raw walk, because GetValue runs MaterializeIfArguments, which would permanently opt the frame's JsArguments out of pooling for arguments[i] readers and detach mapped-index reads from the parameter map. Aliases (var a = arguments) already materialize at assignment time, so the carve-out only needs to cover the direct name.

Found via ETW sampling (ultra @ 8190 Hz) of a steady-state SunSpider mix, where the raw walk accounted for a large share of TryGetIdentifierEnvironmentWithBindingValue's 3.3% self time and EvaluateInternal's 2.6%.

Semantics notes

  • Unresolvable bases throw the identical ReferenceError through the identical engine.GetValue(unresolvable reference) path.
  • TDZ bases now throw "X has not been initialized" instead of "Cannot access 'X' before initialization" — this aligns EvaluateInternal with the message the member read path (GetValue) already throws for the same code; no test asserts the old message on this path.
  • with/ObjectEnvironment and eval-injected bindings behave identically: the identifier caches refuse to cross ObjectEnvironments and HasBinding-probe hopped intermediate envs, falling back to the same full walk.
  • Suspend machinery untouched — identifier reads never suspend.

Benchmarks (default jobs, back-to-back A/B on same machine)

Benchmark main PR Δ
ElementAccess Write 90.51 ms 83.10 ms −8.2%
ElementAccess ReadModifyWrite 155.62 ms 143.97 ms −7.5%
ElementAccess AppendWrite 39.32 ms 34.71 ms −11.7%
ElementAccess ChainedRead 137.01 ms 130.16 ms −5.0%
ElementAccess Read 101.31 ms 98.66 ms −2.6%
SunSpider bitops-nsieve-bits 89.73 ms 82.13 ms −8.5%
SunSpider access-fannkuch 219.55 ms 206.38 ms −6.0%
SunSpider 3d-cube 39.59 ms 37.49 ms −5.3%
SunSpider crypto-md5 57.44 ms 54.38 ms −5.3%
SunSpider crypto-aes 57.85 ms 55.92 ms −3.3%
SunSpider string-fasta 76.74 ms 74.43 ms −3.0%
Uncacheable guard (interop member reads) 51.38 / 47.30 ms 51.22 / 45.77 ms neutral

Allocations unchanged on every row.

ETW re-profile of a function-wrapped SunSpider mix (fixed work, 45 iterations): total 15,687 → 13,617 ms (−13.2%); JintMemberExpression.EvaluateInternal self 401 → 267 ms (−34%). The function-wrapped shape (identifier bases in slots — typical of real code in functions) benefits more than the global-scope BDN scripts.

Gates

  • Jint.Tests: 3537 (net10.0) + 3474 (net472) passed, 0 failed
  • Jint.Tests.PublicInterface: 82 × 2 TFMs passed
  • Jint.Tests.CommonScripts: 28 × 2 TFMs passed
  • Test262: 99,429 passed, 0 failed

🤖 Generated with Claude Code

…ches

JintMemberExpression.EvaluateInternal resolved an identifier base with a raw
environment-chain walk on every evaluation, predating the slot/global caches
that JintIdentifierExpression.GetValue now has. Computed writes (a[i] = v),
compound writes and updates all pay that walk per evaluation.

Reuse the identifier node's own caches instead; keep the raw walk only for
eval/arguments bases so arguments[i] reads do not trigger materialization
and keep the frame's JsArguments poolable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lahma
lahma enabled auto-merge (squash) July 13, 2026 09:35
@lahma
lahma merged commit ef1feb8 into sebastienros:main Jul 13, 2026
4 checks passed
@lahma
lahma deleted the perf-member-ident-read branch July 13, 2026 09:48
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