Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Jint.Benchmark/ObjectAccessBenchmark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,19 @@ namespace Jint.Benchmark;
public class ObjectAccessBenchmark
{
private readonly Prepared<Script> _script;
private readonly Prepared<Script> _writeOnlyScript;

public ObjectAccessBenchmark()
{
// Read-then-write of the same property: exercises both the member-read inline cache and the
// member-write inline cache on a stable object.
const string Script = @"const summary = { res: 0 }; for (var i =0; i < 1_000_000; ++i){ summary.res = summary.res + 1; }";
_script = Engine.PrepareScript(Script);

// Pure write of an existing property (the LHS node is never read), so the write-side inline cache
// can only warm up via write-miss population — then hit for the remaining iterations.
const string WriteOnlyScript = @"const summary = { res: 0 }; for (var i =0; i < 1_000_000; ++i){ summary.res = i; }";
_writeOnlyScript = Engine.PrepareScript(WriteOnlyScript);
}

[Benchmark]
Expand All @@ -19,4 +27,11 @@ public void UpdateObjectProperty()
var engine = new Engine();
engine.Evaluate(_script);
}

[Benchmark]
public void WriteObjectProperty()
{
var engine = new Engine();
engine.Evaluate(_writeOnlyScript);
}
}
12 changes: 11 additions & 1 deletion Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -563,8 +563,18 @@ protected override object EvaluateInternal(EvaluationContext context)
// https://262.ecma-international.org/5.1/#sec-11.13.1
private JsValue SetValue(EvaluationContext context)
{
// slower version
var engine = context.Engine;

// Write-side inline cache for `obj.prop = rhs` (mirrors JintMemberExpression.GetValue's read cache):
// resolves base+rhs once and stores straight into a live writable data descriptor, otherwise completes
// through PutValue. Only declines (returns false) before evaluating anything, so the slow path stays sound.
if (_left is JintMemberExpression memberLeft
&& memberLeft.TryAssignFast(context, _right, out var fastResult))
{
return fastResult;
}

// slower version
var lref = _left.Evaluate(context) as Reference;
if (lref is null)
{
Expand Down
92 changes: 92 additions & 0 deletions Jint/Runtime/Interpreter/Expressions/JintMemberExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -435,4 +435,96 @@ internal JsValue GetCalleeForCall(EvaluationContext context, out JsValue thisObj
thisObject = JsValue.Undefined;
return JsValue.Undefined;
}

/// <summary>
/// Write-side counterpart of <see cref="GetValue"/>'s inline cache for <c>obj.prop = rhs</c>. Reuses the
/// same version-gated own-property cache slots: when the receiver is a <see cref="InternalTypes.PlainObject"/>
/// whose shape is unchanged and the own property is a <em>live</em> writable, non-accessor, non-custom data
/// descriptor, the new value is written straight into the descriptor (no Reference rent, no property-key hash,
/// no dictionary lookup) — exactly the in-place store <see cref="ObjectInstance.Set(JsValue,JsValue,JsValue)"/>
/// performs, which by design does not bump <c>_propertiesVersion</c>.
/// <para>
/// The method returns <c>false</c> only from the eligibility gate, having evaluated nothing, so the caller's
/// unchanged slow path runs. Once the base and right-hand side have been evaluated (each exactly once, in spec
/// order) it always completes the assignment and returns <c>true</c>: either the in-place store, or — for an
/// absent / accessor / read-only / custom-value property, or a non-<see cref="ObjectInstance"/> base — a
/// fallback through <see cref="Engine.PutValue"/> rented from the already-resolved base+key (so a side-effecting
/// base or RHS is never evaluated twice and prototype-setter / CreateDataProperty / strict read-only semantics
/// are preserved).
/// </para>
/// </summary>
internal bool TryAssignFast(EvaluationContext context, JintExpression right, out JsValue result)
{
var engine = context.Engine;

// Same eligibility as GetValue's primary fast path, plus the computed-read path's Suspendable==null gate:
// a static string-named, non-optional, non-short-circuiting, non-super property write with no custom
// resolver, in a context where neither operand can suspend (so no generator/async bookkeeping is needed).
if (_propertyExpression is not null
|| _determinedProperty is not JsString determinedProperty
|| _memberExpression.Optional
|| _objectExpressionCanShortCircuit
|| engine._customResolver
|| _objectExpression is JintSuperExpression
|| engine.ExecutionContext.Suspendable is not null)
{
result = JsValue.Undefined;
return false;
}

// Evaluate base, then RHS — each exactly once, preserving base→key→rhs spec order. A null/undefined base
// is simply not a PlainObject and flows to the fallback, where PutValue→ToObject throws after the RHS.
var baseValue = _objectExpression.GetValue(context);
var rval = right.GetValue(context);

context.LastSyntaxElement = _expression;

if (baseValue is ObjectInstance baseObject
&& (baseObject._type & InternalTypes.PlainObject) != InternalTypes.Empty)
{
PropertyDescriptor? descriptor;
if (ReferenceEquals(baseObject, _cachedReadObject)
&& baseObject._propertiesVersion == _cachedReadVersion
&& _cachedReadDescriptor is not null)
{
descriptor = _cachedReadDescriptor;
}
else
{
var ownDescriptor = baseObject.GetOwnProperty(determinedProperty);
if (ReferenceEquals(ownDescriptor, PropertyDescriptor.Undefined))
{
// Absent own property: inherited-setter / CreateDataProperty semantics — handled by fallback.
_cachedReadObject = null;
_cachedReadDescriptor = null;
descriptor = null;
}
else
{
_cachedReadObject = baseObject;
_cachedReadVersion = baseObject._propertiesVersion;
_cachedReadDescriptor = ownDescriptor;
descriptor = ownDescriptor;
}
}

// Re-read the flags live every store: Object.defineProperty flips Writable in place on the same
// descriptor without bumping the version, so the writability decision must never be cached. The mask
// must equal exactly Writable — i.e. writable, not an accessor (NonData), not custom-valued.
if (descriptor is not null
&& (descriptor._flags & (PropertyFlag.NonData | PropertyFlag.CustomJsValue | PropertyFlag.Writable)) == PropertyFlag.Writable)
{
descriptor._value = rval;
result = rval;
return true;
}
}

// Fallback: complete via the normal pipeline from the already-resolved base + key (no re-evaluation).
var reference = engine._referencePool.Rent(baseValue, determinedProperty, StrictModeScope.IsStrictModeCode, thisValue: null);
engine.PutValue(reference, rval);
engine._referencePool.Return(reference);
result = rval;
return true;
}
}
Loading