Prototype-method inline cache for obj.method resolved on the direct prototype#2558
Merged
Merged
Conversation
… prototype Member reads and member-calls whose property lives on the receiver's *direct prototype* — `arr.push(...)`, `date.getTime()`, `counter.inc()` where `inc` is on the prototype — had no inline cache. Only *own* properties were cached, so every such access fell through the own-property fast paths and re-walked the prototype chain: a dictionary miss-probe on the receiver, then a full recursive `Get` on the prototype (its own fast-path check + dictionary hit-probe + descriptor unwrap). Add a monomorphic prototype-method cache to `JintMemberExpression`, shared by the read and member-call sites (a member node serves one role and reads one property name). On a hit it returns the cached prototype descriptor's value directly. Validity is correct-by-construction: - same receiver instance (`ReferenceEquals`) — per-site monomorphic; - receiver own-shape unchanged (`_propertiesVersion`) — no own property added that would shadow; - direct prototype unchanged (`GetPrototypeOf()` `ReferenceEquals` the cached holder) — not re-pointed; - holder own-shape unchanged (`_propertiesVersion`) — method not redefined or deleted. The prototype is derived through the virtual `GetPrototypeOf()` (exactly as `ObjectInstance.Get` walks it), so objects that shadow the `_prototype` field and override `[[GetPrototypeOf]]` — e.g. custom interop instances — resolve to the correct prototype. The populate path no longer re-probes the receiver's own property: the read/member-call fast paths already established the own-miss (a shape-slot miss, or a `GetOwnProperty` that returned undefined), and thread that through as `ownMissConfirmed`, so a never-hitting (polymorphic-by-identity) site does one own-property probe per call instead of two. Receivers and prototypes with a non-ordinary `[[Get]]` for *named* keys must never be bypassed. Add `InternalTypes.ExoticGet`, set in the constructors of the `ObjectInstance` subclasses whose `[[Get]]` resolves named keys specially — `JsProxy`, `JsTypedArray`, `IteratorResult`, `ObjectWrapper` (and its `ArrayLikeWrapper` subclass, which inherits the flag), `NamespaceReference`, `ModuleNamespace` — and skip the cache when either the receiver or its direct prototype carries it. `ArrayInstance` also overrides `Get`, but only for integer indices and `length` (both of which it reports from `GetOwnProperty`, so the populate path defers to the full `Get` there); it is ordinary for named keys and intentionally stays unflagged, which is what keeps `arr.push` / `arr.pop` cacheable. `PrototypeMethodCacheTests` includes a reflection tripwire that fails if a new `Get` override appears unclassified, plus behavioural tests for shadowing, redefinition, deletion, prototype reassignment, and accessor re-invocation after the cache is warm. Benchmarks (default jobs, same machine, baseline upstream/main d6e3da8, same thermal window): MethodCallBenchmark — the dedicated, multi-iteration signal: | Method | Baseline | Branch | Delta | |-------------------------------------|----------|----------|--------| | ArrayPushPop (arr.push/pop loop) | 246.4 ms | 189.0 ms | -23.3% | | UserPrototypeMethod (c.inc on proto)| 343.0 ms | 294.8 ms | -14.1% | | MethodCallThis (guard) | 291.8 ms | 280.5 ms | -3.9% | | MethodCallCaptured (guard) | 297.2 ms | 305.6 ms | +2.8% | | FreeFunctionCall (guard) | 337.0 ms | 345.8 ms | +2.6% | Guards exercise own-method / free-function paths the cache never reaches; they sit in the +/-3-4% run-to-run noise. Allocation is unchanged (this is a read-path speedup). EngineComparison (Jint, prepared scripts; averaged over two same-window baseline/treatment pairings): consistent wins on array/object-heavy scripts — array-stress -12.3%, dromaeo-object-array-modern -9.7%. The remaining scripts fall within this machine's ~+/-3-4% process-to-process noise (their deltas sign-flip across repeated pairings); dromaeo-object-string, which a redundant own-property probe initially nudged ~+2%, is flat after the ownMissConfirmed change. Test262: no new failures (Failed: 4 / Passed: 99256, identical to baseline d6e3da8; the 4 annexB RegExp failures are pre-existing 65536-iteration eval + RegExp-compile loops that flaky-time-out independent of this change). Jint.Tests (3145/0), Jint.Tests.PublicInterface, Jint.Tests.CommonScripts, Jint.Tests.SourceGenerators all green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RDS23QeSSNRkaUB2KL74U9
lahma
added a commit
that referenced
this pull request
Jun 30, 2026
…location, not two) (#2559) A shape-mode `JsObject` kept its hidden-class shape and slot values in a single combined `object[]` store (`[shape, v0, v1, …]`), so constructing one allocated two objects: the `JsObject` and the store array. This makes the slot values *in-object*: `JsObject` now holds the shape reference and the first `InlineCapacity` (4) slot values in fields (via an `[InlineArray]` block on modern runtimes, hand-rolled fixed fields on legacy TFMs), spilling only the surplus of larger layouts into an overflow array. A constructor or object literal whose instance fits the in-object capacity — the overwhelming majority of real objects: points, pairs, nodes, small records — now allocates a **single object**, and `StartShapeBuilding` (a constructor's `this`) allocates no slot storage at all until `this.x=` fills a slot. Reads also drop the pointer-chase: a slot below the inline capacity is a field-offset access, not a load of the store array followed by an index. All access goes through the existing `ShapeOf`/`GetSlot`/`SetSlot` seam, so the change is contained to `JsObject` plus the literal builder (which fills slots directly instead of building an array). Larger objects (> in-object capacity) keep an overflow array and are allocation-neutral; dictionary-mode objects are unaffected functionally and pay only the wider inline-slot fields. Benchmarks (default jobs, same machine, baseline #2558 / 3c42a26): PropertyAlloc — construction (cached field values isolate per-object cost): | Benchmark | Baseline | Branch | Delta | |---------------------|-------------------|-------------------|------------------| | Constructor3Cached | 133.2 ms / 69.9 MB| 122.9 ms / 66.9 MB| -7.7% / -4.4%, Gen0 -5.9% | | Literal2Cached | 38.5 ms / 28.7 MB| 38.0 ms / 27.2 MB| -1.3% / -5.3% | | Constructor1 (mixed)| 69.9 MB | 66.9 MB | -4.6% alloc/time | | Literal3 (mixed) | 50.2 MB | 47.0 MB | -6.4% alloc | ObjectAccess (2-run average): | UpdateObjectProperty (read+write) | -1.7% (inline read skips the array pointer-chase) | | WriteObjectProperty (pure write) | +2.2% (the inline/overflow branch in SetSlot) | EngineComparison (Jint_ParsedScript, 19 scripts): allocation-neutral (-0.4%..+0.5%), time-flat. The win is construction allocation (one fewer object per small constructed object) plus a small read speed-up; the cost is a ~2% slow-down on a *pure*-write microbenchmark (the inline/overflow branch in `SetSlot`, offset by the read speed-up in any read+write pattern) and a wider per-`JsObject` footprint. EngineComparison (real-world mixed scripts) is allocation-neutral and time-flat, so neither cost shows up outside the microbenchmarks. Test262: 0 failures / 99260 passed (unchanged from baseline). Jint.Tests, Jint.Tests.PublicInterface, Jint.Tests.CommonScripts, Jint.Tests.SourceGenerators all green. Claude-Session: https://claude.ai/code/session_01RDS23QeSSNRkaUB2KL74U9 Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jul 6, 2026
This was referenced Jul 13, 2026
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.
Member reads and member-calls whose property lives on the receiver's direct prototype —
arr.push(...),date.getTime(),counter.inc()whereincis on the prototype — had no inlinecache. Only own properties were cached, so every such access fell through the own-property fast
paths and re-walked the prototype chain: a dictionary miss-probe on the receiver, then a full
recursive
Geton the prototype (its own fast-path check + dictionary hit-probe + descriptor unwrap).Add a monomorphic prototype-method cache to
JintMemberExpression, shared by the read andmember-call sites (a member node serves one role and reads one property name). On a hit it returns
the cached prototype descriptor's value directly. Validity is correct-by-construction:
ReferenceEquals) — per-site monomorphic;_propertiesVersion) — no own property added that would shadow;GetPrototypeOf()ReferenceEqualsthe cached holder) — not re-pointed;_propertiesVersion) — method not redefined or deleted.The prototype is derived through the virtual
GetPrototypeOf()(exactly asObjectInstance.Getwalks it), so objects that shadow the
_prototypefield and override[[GetPrototypeOf]]— e.g.custom interop instances — resolve to the correct prototype.
The populate path no longer re-probes the receiver's own property: the read/member-call fast paths
already established the own-miss (a shape-slot miss, or a
GetOwnPropertythat returned undefined),and thread that through as
ownMissConfirmed, so a never-hitting (polymorphic-by-identity) site doesone own-property probe per call instead of two.
Receivers and prototypes with a non-ordinary
[[Get]]for named keys must never be bypassed. AddInternalTypes.ExoticGet, set in the constructors of theObjectInstancesubclasses whose[[Get]]resolves named keys specially —
JsProxy,JsTypedArray,IteratorResult,ObjectWrapper(and its
ArrayLikeWrappersubclass, which inherits the flag),NamespaceReference,ModuleNamespace— and skip the cache when either the receiver or its direct prototype carries it.
ArrayInstancealsooverrides
Get, but only for integer indices andlength(both of which it reports fromGetOwnProperty, so the populate path defers to the fullGetthere); it is ordinary for named keysand intentionally stays unflagged, which is what keeps
arr.push/arr.popcacheable.PrototypeMethodCacheTestsincludes a reflection tripwire that fails if a newGetoverride appearsunclassified, plus behavioural tests for shadowing, redefinition, deletion, prototype reassignment,
and accessor re-invocation after the cache is warm.
Benchmarks (default jobs, same machine, baseline upstream/main d6e3da8, same thermal window):
MethodCallBenchmark — the dedicated, multi-iteration signal:
EngineComparison (Jint, prepared scripts; averaged over two same-window baseline/treatment pairings):
consistent wins on array/object-heavy scripts — array-stress -12.3%, dromaeo-object-array-modern
-9.7%. The remaining scripts fall within this machine's ~+/-3-4% process-to-process noise (their
deltas sign-flip across repeated pairings); dromaeo-object-string, which a redundant own-property
probe initially nudged ~+2%, is flat after the ownMissConfirmed change.
Test262: no new failures (Failed: 4 / Passed: 99256, identical to baseline d6e3da8; the 4 annexB
RegExp failures are pre-existing 65536-iteration eval + RegExp-compile loops that flaky-time-out
independent of this change). Jint.Tests (3145/0), Jint.Tests.PublicInterface, Jint.Tests.CommonScripts,
Jint.Tests.SourceGenerators all green.
🤖 Generated with Claude Code
https://claude.ai/code/session_01RDS23QeSSNRkaUB2KL74U9