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
141 changes: 141 additions & 0 deletions Jint.Benchmark/ArrayHoleTraversalBenchmark.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
using BenchmarkDotNet.Attributes;
using Jint.Native;

namespace Jint.Benchmark;

/// <summary>
/// Traversal cost of holey (but still dense-backed) arrays versus packed ones. Reading a hole
/// falls off the dense fast path and probes the prototype chain per element (the spec requires a
/// HasProperty/Get walk), so index loops, join and indexOf pay a per-hole penalty that a packed
/// array never sees. The dense/holey lane pairs measure that gap; `in` exercises the raw
/// HasProperty probe.
/// </summary>
[MemoryDiagnoser]
[HideColumns("Error", "Gen0", "Gen1", "Gen2")]
public class ArrayHoleTraversalBenchmark
{
private Engine _engine = null!;
private Prepared<Script> _sumDense;
private Prepared<Script> _sumHoley;
private Prepared<Script> _joinDense;
private Prepared<Script> _joinHoley;
private Prepared<Script> _indexOfMissDense;
private Prepared<Script> _indexOfMissHoley;
private Prepared<Script> _inOperatorHoley;

private const string SetupSource = """
var dense = [];
var holey = [];
(function () {
for (var i = 0; i < 8000; i++) { dense[i] = i; }
// every 4th index present; same length as dense, 75% holes
for (var i = 0; i < 8000; i += 4) { holey[i] = i; }
holey[7999] = 7999;
})();
""";

[GlobalSetup]
public void Setup()
{
_engine = new Engine();
_engine.Execute(SetupSource);

_sumDense = Engine.PrepareScript("""
function f() {
var s = 0;
for (var n = 0; n < 20; n++) {
for (var i = 0; i < 8000; i++) { s += dense[i] | 0; }
}
return s;
}
f();
""");

_sumHoley = Engine.PrepareScript("""
function f() {
var s = 0;
for (var n = 0; n < 20; n++) {
for (var i = 0; i < 8000; i++) { s += holey[i] | 0; }
}
return s;
}
f();
""");

_joinDense = Engine.PrepareScript("""
function f() {
var len = 0;
for (var n = 0; n < 10; n++) { len += dense.join(',').length; }
return len;
}
f();
""");

_joinHoley = Engine.PrepareScript("""
function f() {
var len = 0;
for (var n = 0; n < 10; n++) { len += holey.join(',').length; }
return len;
}
f();
""");

_indexOfMissDense = Engine.PrepareScript("""
function f() {
var s = 0;
for (var n = 0; n < 50; n++) { s += dense.indexOf(-1); }
return s;
}
f();
""");

_indexOfMissHoley = Engine.PrepareScript("""
function f() {
var s = 0;
for (var n = 0; n < 50; n++) { s += holey.indexOf(-1); }
return s;
}
f();
""");

_inOperatorHoley = Engine.PrepareScript("""
function f() {
var s = 0;
for (var n = 0; n < 20; n++) {
for (var i = 0; i < 8000; i++) { if (i in holey) { s++; } }
}
return s;
}
f();
""");

_engine.Evaluate(_sumDense);
_engine.Evaluate(_sumHoley);
_engine.Evaluate(_joinDense);
_engine.Evaluate(_joinHoley);
_engine.Evaluate(_indexOfMissDense);
_engine.Evaluate(_indexOfMissHoley);
_engine.Evaluate(_inOperatorHoley);
}

[Benchmark]
public JsValue SumDense() => _engine.Evaluate(_sumDense);

[Benchmark]
public JsValue SumHoley() => _engine.Evaluate(_sumHoley);

[Benchmark]
public JsValue JoinDense() => _engine.Evaluate(_joinDense);

[Benchmark]
public JsValue JoinHoley() => _engine.Evaluate(_joinHoley);

[Benchmark]
public JsValue IndexOfMissDense() => _engine.Evaluate(_indexOfMissDense);

[Benchmark]
public JsValue IndexOfMissHoley() => _engine.Evaluate(_indexOfMissHoley);

[Benchmark]
public JsValue InOperatorHoley() => _engine.Evaluate(_inOperatorHoley);
}
64 changes: 62 additions & 2 deletions Jint.Benchmark/ForInGuardBenchmark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ namespace Jint.Benchmark;

/// <summary>
/// Object-iteration and type-guard idioms: for-in with and without the lint-mandated
/// hasOwnProperty guard, prototype-chain filtering, for-of over a string, and typeof /
/// instanceof / in dispatch over LCG-mixed inputs the branch predictor cannot memorize.
/// hasOwnProperty guard, prototype-chain filtering, for-in over arrays (dense, holey, and with
/// extra named own props), for-of over a string, and typeof / instanceof / in dispatch over
/// LCG-mixed inputs the branch predictor cannot memorize.
/// </summary>
[MemoryDiagnoser]
[HideColumns("Error", "Gen0", "Gen1", "Gen2")]
Expand All @@ -17,6 +18,9 @@ public class ForInGuardBenchmark
private Prepared<Script> _forInWide;
private Prepared<Script> _forInHasOwnGuard;
private Prepared<Script> _forInProtoChain;
private Prepared<Script> _forInDenseArray;
private Prepared<Script> _forInHoleyArray;
private Prepared<Script> _forInArrayExtraProps;
private Prepared<Script> _forOfString;
private Prepared<Script> _typeofSwitchMixed;
private Prepared<Script> _instanceofMixed;
Expand All @@ -26,6 +30,9 @@ public class ForInGuardBenchmark
var six = { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 };
var wide = {};
var protoObj = Object.create({ pa: 1, pb: 2, pc: 3, pd: 4, pe: 5, pf: 6 });
var denseArr = [];
var holeyArr = [];
var namedArr = [];
var mixedVals = [];
var instObjs = [];
var inObjs = [];
Expand All @@ -36,6 +43,11 @@ class Deriv extends Base { constructor() { super(); this.sub = 2; } }
(function () {
var seed = 20260711;
for (var i = 0; i < 64; i++) { wide['w' + i] = i; }
for (var i = 0; i < 1000; i++) { denseArr[i] = i; }
for (var i = 0; i < 1000; i += 4) { holeyArr[i] = i; }
for (var i = 0; i < 100; i++) { namedArr[i] = i; }
namedArr.tag = 'x';
namedArr.other = 'y';
protoObj.oa = 1; protoObj.ob = 2; protoObj.oc = 3; protoObj.od = 4; protoObj.oe = 5; protoObj.of = 6;
for (var i = 0; i < 1024; i++) {
seed = (seed * 1664525 + 1013904223) | 0;
Expand Down Expand Up @@ -117,6 +129,42 @@ function f() {

_forInHasOwnGuard = Engine.PrepareScript(ForInHasOwnGuardSource);

// 1,000 dense int-indexed keys, no holes, no named own props
_forInDenseArray = Engine.PrepareScript("""
function f() {
var s = 0;
for (var i = 0; i < 200; i++) {
for (var k in denseArr) { s++; }
}
return s;
}
f();
""");

// every 4th index present; enumeration must skip the holes
_forInHoleyArray = Engine.PrepareScript("""
function f() {
var s = 0;
for (var i = 0; i < 200; i++) {
for (var k in holeyArr) { s++; }
}
return s;
}
f();
""");

// 100 indices plus two named own props; order must stay indices-then-named
_forInArrayExtraProps = Engine.PrepareScript("""
function f() {
var s = 0;
for (var i = 0; i < 2000; i++) {
for (var k in namedArr) { s++; }
}
return s;
}
f();
""");

// 6 own + 6 enumerable inherited keys; the guard filters the inherited half
_forInProtoChain = Engine.PrepareScript("""
function f() {
Expand Down Expand Up @@ -170,6 +218,9 @@ function f() {
_engine.Evaluate(_forInWide);
_engine.Evaluate(_forInHasOwnGuard);
_engine.Evaluate(_forInProtoChain);
_engine.Evaluate(_forInDenseArray);
_engine.Evaluate(_forInHoleyArray);
_engine.Evaluate(_forInArrayExtraProps);
_engine.Evaluate(_forOfString);
_engine.Evaluate(_typeofSwitchMixed);
_engine.Evaluate(_instanceofMixed);
Expand All @@ -188,6 +239,15 @@ function f() {
[Benchmark]
public JsValue ForInProtoChain() => _engine.Evaluate(_forInProtoChain);

[Benchmark]
public JsValue ForInDenseArray() => _engine.Evaluate(_forInDenseArray);

[Benchmark]
public JsValue ForInHoleyArray() => _engine.Evaluate(_forInHoleyArray);

[Benchmark]
public JsValue ForInArrayExtraProps() => _engine.Evaluate(_forInArrayExtraProps);

[Benchmark]
public JsValue ForOfString() => _engine.Evaluate(_forOfString);

Expand Down
106 changes: 106 additions & 0 deletions Jint.Benchmark/StringConcatLargeBenchmark.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using BenchmarkDotNet.Attributes;
using Jint.Native;

namespace Jint.Benchmark;

/// <summary>
/// String building from LARGE pieces — the workload where a rope representation would beat the
/// current copy-into-builder strategy. Small-chunk appends (the common `s += c` loop) are the
/// existing ConcatenatedString sweet spot and serve as the baseline; the large-chunk lanes append
/// 4 KB pieces, concatenate two 64 KB strings, and scan the built result with charAt afterwards
/// (the read pattern a lazy/rope representation would have to pay for).
/// </summary>
[MemoryDiagnoser]
[HideColumns("Error", "Gen0", "Gen1", "Gen2")]
public class StringConcatLargeBenchmark
{
private Engine _engine = null!;
private Prepared<Script> _appendSmallChunks;
private Prepared<Script> _appendLargeChunks;
private Prepared<Script> _concatLargePair;
private Prepared<Script> _buildLargeThenScan;

private const string SetupSource = """
var chunk16 = 'abcdefghijklmnop';
var chunk4k = '';
var big64k = '';
(function () {
var parts = [];
for (var i = 0; i < 256; i++) { parts.push(chunk16); }
chunk4k = parts.join('');
parts = [];
for (var i = 0; i < 16; i++) { parts.push(chunk4k); }
big64k = parts.join('');
})();
""";

[GlobalSetup]
public void Setup()
{
_engine = new Engine();
_engine.Execute(SetupSource);

// 4,096 x 16 chars -> 64 KB result; the established fast case
_appendSmallChunks = Engine.PrepareScript("""
function f() {
var s = '';
for (var i = 0; i < 4096; i++) { s += chunk16; }
return s.length;
}
f();
""");

// 256 x 4 KB -> 1 MB result; every append copies the whole chunk today
_appendLargeChunks = Engine.PrepareScript("""
function f() {
var s = '';
for (var i = 0; i < 256; i++) { s += chunk4k; }
return s.length;
}
f();
""");

// one-shot big + big, repeated; O(1) for a rope, O(n) copy today
_concatLargePair = Engine.PrepareScript("""
function f() {
var total = 0;
for (var i = 0; i < 64; i++) {
var t = big64k + big64k;
total += t.length;
}
return total;
}
f();
""");

// build 256 KB from large chunks, then charAt-scan it — the pattern a lazy
// representation must not regress
_buildLargeThenScan = Engine.PrepareScript("""
function f() {
var s = '';
for (var i = 0; i < 64; i++) { s += chunk4k; }
var acc = 0;
for (var i = 0; i < s.length; i += 997) { acc += s.charCodeAt(i); }
return acc;
}
f();
""");

_engine.Evaluate(_appendSmallChunks);
_engine.Evaluate(_appendLargeChunks);
_engine.Evaluate(_concatLargePair);
_engine.Evaluate(_buildLargeThenScan);
}

[Benchmark]
public JsValue AppendSmallChunks() => _engine.Evaluate(_appendSmallChunks);

[Benchmark]
public JsValue AppendLargeChunks() => _engine.Evaluate(_appendLargeChunks);

[Benchmark]
public JsValue ConcatLargePair() => _engine.Evaluate(_concatLargePair);

[Benchmark]
public JsValue BuildLargeThenScan() => _engine.Evaluate(_buildLargeThenScan);
}
Loading