Add write-side inline cache for property assignment (obj.prop = value)#2546
Merged
Conversation
Reads of `obj.prop` already use a version-gated own-property inline cache in JintMemberExpression.GetValue that skips the dictionary lookup on repeat reads. Writes had no equivalent: every `obj.prop = value` rented a Reference, called PutValue, re-hashed the property key, and did a dictionary lookup before the in-place store. Add TryAssignFast as the write-side counterpart, reusing the same cache slots. When the receiver is a PlainObject whose shape is unchanged and the own property is a live writable, non-accessor, non-custom data descriptor, the value is written straight into the descriptor with no Reference rent, key hash, or lookup — exactly the in-place store ObjectInstance.Set already performs (which by design does not bump _propertiesVersion). The writability/data/custom flags are re-read live on every store, because Object.defineProperty mutates them in place without a version bump. The fast path declines only at the eligibility gate (before evaluating anything); once base and RHS are evaluated (each once, in spec order) it always completes the assignment, falling back to PutValue rented from the already-resolved base+key for absent / accessor / read-only / custom-value / non-PlainObject cases — so a side-effecting base or RHS is never evaluated twice and prototype-setter, CreateDataProperty, and strict read-only semantics are preserved. Benchmarks (net10.0, AMD 5950X): ObjectAccess.UpdateObjectProperty -9.5..13%, new WriteObjectProperty (pure write, write-miss population) -14%, PropertyAlloc.Constructor1 -11%; allocations unchanged; no read-path or Test262 regressions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lahma
enabled auto-merge (squash)
June 25, 2026 19:42
This was referenced Jul 6, 2026
This was referenced Jul 13, 2026
This was referenced Jul 20, 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.
Summary
Reads of
obj.propalready use a version-gated own-property inline cache inJintMemberExpression.GetValuethat skips the dictionary lookup on repeat reads. Writes had no equivalent: everyobj.prop = valuerented aReference, calledPutValue, re-hashed the property key, and did a dictionary lookup before the in-place store.This PR adds the write-side counterpart,
TryAssignFast, hooked intoSimpleAssignmentExpression.SetValue. It reuses the same cache slots (_cachedReadObject/_cachedReadVersion/_cachedReadDescriptor), so asummary.res = summary.res + 1site shares one cache between its read and its write. When the receiver is aPlainObjectwhose shape is unchanged and the own property is a live writable, non-accessor, non-custom data descriptor, the new value is written straight into the descriptor — noReferencerent, no key hash, no dictionary lookup — exactly the in-place storeObjectInstance.Setalready performs (which by design does not bump_propertiesVersion).How it stays correct
(_flags & (NonData | CustomJsValue | Writable)) == Writable, never cached as a decision. This is required becauseObject.defineProperty(obj, p, { writable: false })flips the flag in place on the same descriptor object without bumping the version — only the live flag read catches it. Data↔accessor and get/set changes allocate a new descriptor viaSetOwnProperty, which does bump the version → cache miss.super, andSuspendable is null). Once base and RHS are evaluated — each exactly once, in spec order — it always completes the assignment: either the in-place store, or a fallback throughPutValuerented from the already-resolved base + key. So a side-effecting base or RHS is never evaluated twice, and absent / accessor / read-only / custom-value / non-PlainObjectcases keep prototype-setter dispatch,CreateDataProperty, and the strict read-onlyTypeError.SimpleAssignmentExpressiononly; compound (+=) / update (++) / discard paths are untouched.Benchmarks
BenchmarkDotNet
DefaultJob,net10.0, AMD Ryzen 9 5950X, .NET 10.0.9. Baseline vs. this branch measured back-to-back in the same session (only the two interpreter files reverted for the baseline).Literal3/Literal8build via object literals — they don't touch the assignment path and serve as drift controls (here flat: +2.1% / −1.4%, opposite signs = noise, not drift).ObjectAccess.UpdateObjectProperty(read+write loop)ObjectAccess.WriteObjectProperty(pure write, new)PropertyAlloc.Constructor1(this.value = v)PropertyAlloc.Constructor3(this.a=a;this.b=b;this.c=c)PropertyAlloc.Literal3(control)PropertyAlloc.Literal8(control)UpdateObjectProperty/WriteObjectProperty: steady-state writes lose the per-storeReferencerent +ToObject+ key hash + dictionary lookup.WriteObjectProperty(added here) is a pure write loop — its LHS node is never read — proving the write-miss population path warms the cache without any read.Constructor1/Constructor3(new-property writes, where the fast path falls back): no regression, and in fact faster, becauseTryAssignFast's base resolution is leaner than the previousEvaluate→Reference→PutValuepath even with the one extraGetOwnPropertymiss-probe.JsNumberboxing).Testing
Jint.TestsandJint.Tests.PublicInterface: all green.Jint.Tests.Test262: baseline-equal (the only failures are 4 pre-existingannexB/.../RegExp-*-escape-BMPcases that hit the 30 s timeout — identical with and without this change).TypeError,Object.definePropertyflippingwritabletrue→false in place is honored, inherited prototype setters, frozen objects, and array element /lengthwrites (non-PlainObjectfallback).🤖 Generated with Claude Code