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
18 changes: 18 additions & 0 deletions Jint.Benchmark/ElementAccessBenchmark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class ElementAccessBenchmark
private Prepared<Script> _write;
private Prepared<Script> _readModifyWrite;
private Prepared<Script> _chainedRead;
private Prepared<Script> _appendWrite;

[GlobalSetup]
public void Setup()
Expand Down Expand Up @@ -50,11 +51,25 @@ public void Setup()
s;
""", strict: true);

// the MMulti/VMulti build pattern: every write is an append at the current length
// into a fresh array (index masking would turn appends into overwrites, so the signal
// includes the JsNumber boxing of the stored values like the cube itself does)
_appendWrite = Engine.PrepareScript("""
var sink = 0;
for (var i = 0; i < 100000; i++) {
var m = [];
m[0] = i; m[1] = i; m[2] = i; m[3] = i;
sink = (sink + m[3]) & 1023;
}
sink;
""", strict: true);

_engine = new Engine(static options => options.Strict());
_engine.Evaluate(_read);
_engine.Evaluate(_write);
_engine.Evaluate(_readModifyWrite);
_engine.Evaluate(_chainedRead);
_engine.Evaluate(_appendWrite);
}

[Benchmark]
Expand All @@ -68,4 +83,7 @@ public void Setup()

[Benchmark]
public JsValue ChainedRead() => _engine.Evaluate(_chainedRead);

[Benchmark]
public JsValue AppendWrite() => _engine.Evaluate(_appendWrite);
}
85 changes: 85 additions & 0 deletions Jint.Tests/Runtime/ArrayAppendLaneTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
namespace Jint.Tests.Runtime;

/// <summary>
/// Pins the dense-append fast path (ArrayInstance.TryAppendDense): computed-index writes at exactly
/// the current length append in place, and every restricted state defers to the full spec path.
/// </summary>
public class ArrayAppendLaneTests
{
[Fact]
public void AppendsBuildDenseArraysWithCorrectLength()
{
var engine = new Engine();
var result = engine.Evaluate("""
(function () {
var m = [];
for (var i = 0; i < 10; i++) { m[i] = i * 2; }
var lazy = new Array(3);
lazy[3] = 'x';
var viaLength = [];
viaLength[viaLength.length] = 'a';
viaLength[viaLength.length] = 'b';
return m.join(',') + '|' + m.length + '|' + lazy.length + '|' + lazy[0] + '|' + viaLength.join('');
})()
""").AsString();

Assert.Equal("0,2,4,6,8,10,12,14,16,18|10|4|undefined|ab", result);
}

[Fact]
public void NonExtensibleAndNonWritableLengthStillThrowInStrictMode()
{
var engine = new Engine();
var result = engine.Evaluate("""
(function () {
'use strict';
var r = [];
var sealedArr = [1];
Object.preventExtensions(sealedArr);
try { sealedArr[1] = 2; r.push('no-throw'); } catch (e) { r.push(e instanceof TypeError); }
var fixedLen = [1];
Object.defineProperty(fixedLen, 'length', { writable: false });
try { fixedLen[1] = 2; r.push('no-throw'); } catch (e) { r.push(e instanceof TypeError); }
var frozen = Object.freeze([]);
try { frozen[0] = 1; r.push('no-throw'); } catch (e) { r.push(e instanceof TypeError); }
return r.join(',') + '|' + sealedArr.length + '|' + fixedLen.length + '|' + frozen.length;
})()
""").AsString();

Assert.Equal("true,true,true|1|1|0", result);
}

[Fact]
public void HoleFillAndOutOfOrderWritesKeepSpecBehavior()
{
var engine = new Engine();
var result = engine.Evaluate("""
(function () {
var a = [];
a[2] = 'c'; // out-of-order: not an append (length 0 -> 3 with holes)
a[0] = 'a'; // hole fill below length
var holes = (1 in a) ? 'no-hole' : 'hole';
return a.length + '|' + a[0] + '|' + a[2] + '|' + holes;
})()
""").AsString();

Assert.Equal("3|a|c|hole", result);
}

[Fact]
public void AppendAfterCapacityGrowthKeepsElements()
{
var engine = new Engine();
var result = engine.Evaluate("""
(function () {
var a = [];
for (var i = 0; i < 100; i++) { a[i] = i; }
var ok = true;
for (var j = 0; j < 100; j++) { if (a[j] !== j) { ok = false; } }
return ok + '|' + a.length;
})()
""").AsString();

Assert.Equal("true|100", result);
}
}
43 changes: 43 additions & 0 deletions Jint/Native/Array/ArrayInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -917,6 +917,49 @@ internal bool TryWriteExistingDense(uint index, JsValue value)
return false;
}

/// <summary>
/// Fast append of the next dense element (<paramref name="index"/> == length): grows capacity
/// with the standard doubling policy and bumps length in place. Returns false for anything
/// else — non-append indices, non-writable or materialized-non-default length, non-extensible
/// arrays, sparse mode, dense cap — so the caller falls back to the full set path (which
/// enforces the corresponding spec errors). The caller must have already verified
/// <see cref="CanUseFastAccess"/>. An index below the backing length writes into spare
/// capacity: by the dense invariant slots at or beyond length are null holes.
/// </summary>
internal bool TryAppendDense(uint index, JsValue value)
{
var temp = _dense;
if (temp is null || !Extensible || !LengthIsWritable)
{
return false;
}

if (index != (uint) GetJsNumberLength()._value || index >= MaxDenseArrayLength)
{
return false;
}

if (index < (uint) temp.Length)
{
temp[index] = value;
}
else
{
// same growth policy as WriteArrayValueUnlikely's dense arm
var newSize = System.Math.Max(index, System.Math.Max((uint) temp.Length, 2)) * 2;
if (newSize >= MaxDenseArrayLength)
{
return false;
}

EnsureCapacity(newSize);
_dense![index] = value;
}

SetLengthValue(JsNumber.Create(index + 1));
return true;
}

[MethodImpl(MethodImplOptions.NoInlining)]
private bool TryGetValueUnlikely(uint index, out JsValue value)
{
Expand Down
10 changes: 6 additions & 4 deletions Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -925,14 +925,16 @@ private JsValue SetValue(EvaluationContext context)
return rval;
}

// Fast path for dense array element overwrite: arr[intIndex] = rval. Only overwrites an
// existing in-range slot (TryWriteExistingDense), so it never grows length, fills a hole,
// or triggers sparse conversion — those defer to the full PutValue path below.
// Fast path for dense array element writes: arr[intIndex] = rval. Overwrite of an
// existing in-range slot (TryWriteExistingDense), or — probed only on that miss — an
// append at exactly the current length (TryAppendDense, the array-building pattern
// `m[i] = v` over fresh arrays). Hole fills, out-of-order growth and every restricted
// state defer to the full PutValue path below.
if (lref.Base is JsArray array
&& array.CanUseFastAccess
&& lref.ReferencedName is JsNumber indexNumber
&& ArrayInstance.IsArrayIndex(indexNumber, out var arrayIndex)
&& array.TryWriteExistingDense(arrayIndex, rval))
&& (array.TryWriteExistingDense(arrayIndex, rval) || array.TryAppendDense(arrayIndex, rval)))
{
engine._referencePool.Return(lref);
return rval;
Expand Down
Loading