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

namespace Jint.Benchmark;

/// <summary>
/// Decomposes the per-iteration cost of a hot function-local loop with every fast path engaged
/// (fixed slots, pooled env, slot caches, unboxed counters): the empty loop isolates the
/// for-statement machinery (test, update, statement dispatch), and each body row adds exactly
/// one statement on top so the delta attributes cost to that construct. 100k iterations per op.
/// </summary>
[MemoryDiagnoser]
[HideColumns("Error", "Gen0", "Gen1", "Gen2")]
public class LoopDispatchBenchmarks
{
private Engine _engine = null!;
private Prepared<Script> _emptyLoop;
private Prepared<Script> _counterAdd;
private Prepared<Script> _localCopy;
private Prepared<Script> _stringAppend;
private Prepared<Script> _comparisonOnly;

[GlobalSetup]
public void Setup()
{
// pure loop machinery: test + update + (empty) body dispatch
_emptyLoop = Engine.PrepareScript("""
function f() { for (var i = 0; i < 100000; i++) { } return i; }
f();
""");

// + one numeric compound assignment (unboxed discard lane)
_counterAdd = Engine.PrepareScript("""
function f() { var n = 0; for (var i = 0; i < 100000; i++) { n += 1; } return n; }
f();
""");

// + one plain slot-to-slot assignment
_localCopy = Engine.PrepareScript("""
function f() { var a = 1, b = 0; for (var i = 0; i < 100000; i++) { b = a; } return b; }
f();
""");

// + one string compound assignment (rope append, slot lane) — the dromaeo-core-eval body shape
_stringAppend = Engine.PrepareScript("""
function f() { var s = ''; for (var i = 0; i < 100000; i++) { s += 'a'; } return s.length; }
f();
""");

// + one comparison whose result is discarded through an if with empty branches
_comparisonOnly = Engine.PrepareScript("""
function f() { var n = 0; for (var i = 0; i < 100000; i++) { if (i < 50000) { } } return n; }
f();
""");

_engine = new Engine();
_engine.Evaluate(_emptyLoop);
_engine.Evaluate(_counterAdd);
_engine.Evaluate(_localCopy);
_engine.Evaluate(_stringAppend);
_engine.Evaluate(_comparisonOnly);
}

[Benchmark(Baseline = true)]
public JsValue EmptyLoop() => _engine.Evaluate(_emptyLoop);

[Benchmark]
public JsValue CounterAdd() => _engine.Evaluate(_counterAdd);

[Benchmark]
public JsValue LocalCopy() => _engine.Evaluate(_localCopy);

[Benchmark]
public JsValue StringAppend() => _engine.Evaluate(_stringAppend);

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

/// <summary>
/// Relational comparisons against slot-stored numbers take an unboxed fast lane in both boolean
/// (loop/if test) and value positions; these pin the semantic corners against the materialized
/// path (NaN, non-number transitions, const bindings, shadowing).
/// </summary>
public class RelationalComparisonTests
{
[Fact]
public void NanComparisonsAreFalseInTestAndValuePositions()
{
var engine = new Engine(static options => options.Strict());
var result = engine.Evaluate("""
function f() {
var x = NaN;
var r = '';
for (var i = 0; i < 2; i++) {
if (x < 5) { r += 'a'; }
if (x > 5) { r += 'b'; }
if (x <= 5) { r += 'c'; }
if (x >= 5) { r += 'd'; }
}
return r + '|' + (x < 5) + (x > 5) + (x <= 5) + (x >= 5);
}
f();
""").AsString();

Assert.Equal("|falsefalsefalsefalse", result);
}

[Fact]
public void ComparisonFallsBackWhenBindingTurnsNonNumeric()
{
var engine = new Engine(static options => options.Strict());
var result = engine.Evaluate("""
function f() {
var x = 10;
var r = '';
if (x < 20) { r += '1'; }
x = 'zz';
if (x < 20) { r += '2'; }
x = '5';
if (x < 20) { r += '3'; }
return r;
}
f();
""").AsString();

Assert.Equal("13", result);
}

[Fact]
public void ValuePositionsProduceBooleans()
{
var engine = new Engine(static options => options.Strict());
var result = engine.Evaluate("""
function f() {
var i = 5;
var values = [];
for (var n = 0; n < 2; n++) {
values.push(i < 5, i <= 5, i > 4, i >= 6);
}
return values.join(',');
}
f();
""").AsString();

Assert.Equal("false,true,true,false,false,true,true,false", result);
}

[Fact]
public void ConstAndZeroEdgeCases()
{
var engine = new Engine(static options => options.Strict());
var result = engine.Evaluate("""
function f() {
const c = 1;
var z = -0;
var r = '';
for (var i = 0; i < 2; i++) {
if (c < 2) { r += 'a'; }
if (z <= 0) { r += 'b'; }
if (z >= 0) { r += 'c'; }
}
return r;
}
f();
""").AsString();

Assert.Equal("abcabc", result);
}

[Fact]
public void CachedEvalComparisonRespectsBlockShadowing()
{
// the lane resolves through the shadow-aware slot-cache walk; a block-shadowed
// binding must not compare against the outer variable's cached slot
var engine = new Engine(static options => options.Strict());
engine.Execute("""
var log = [];
function h() {
var x = 1;
eval("if (x < 5) { log.push('outer:' + x); }");
eval("if (x < 5) { log.push('outer:' + x); }");
{ let x = 100; eval("if (x < 5) { log.push('inner-wrong'); } else { log.push('inner:' + x); }"); }
}
h();
""");

Assert.Equal("outer:1,outer:1,inner:100", engine.Evaluate("log.join(',')").AsString());
}
}
93 changes: 91 additions & 2 deletions Jint/Runtime/Interpreter/Expressions/JintBinaryExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Jint.Extensions;
using Jint.Native;
using Jint.Native.Object;
using Jint.Runtime.Environments;
using Jint.Runtime.Interop;

namespace Jint.Runtime.Interpreter.Expressions;
Expand All @@ -16,8 +17,8 @@ internal abstract class JintBinaryExpression : JintExpression
private readonly record struct OperatorKey(string? OperatorName, Type Left, Type Right);
private static readonly ConcurrentDictionary<OperatorKey, MethodDescriptor> _knownOperators = new();

private readonly JintExpression _left;
private readonly JintExpression _right;
private protected readonly JintExpression _left;
private protected readonly JintExpression _right;

private JintBinaryExpression(NonLogicalBinaryExpression expression) : base(expression)
{
Expand All @@ -26,6 +27,53 @@ private JintBinaryExpression(NonLogicalBinaryExpression expression) : base(expre
_right = Build(expression.Right);
}

/// <summary>
/// Per-node fast lane for the canonical loop-test shape `identifier &lt;op&gt; numericConstant`:
/// when the identifier resolves to a slot-stored number, the comparison runs on raw doubles
/// without materializing either operand — which also keeps an unboxed counter unboxed instead
/// of ping-ponging between its update (stores raw) and its loop test (would materialize).
/// IEEE double comparisons reproduce the abstract relational operator for all four forms,
/// including NaN operands (spec result undefined, coerced to false).
/// </summary>
private protected struct NumericConstantComparisonLane
{
private JintIdentifierExpression? _identifier;
private double _constant;
private SlotLocationCache _slotCache;

public void Initialize(JintExpression left, JintExpression right)
{
if (left is JintIdentifierExpression identifier
&& right is JintConstantExpression { Value: JsNumber number })
{
_identifier = identifier;
_constant = number._value;
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetOperands(EvaluationContext context, out double left, out double right)
{
left = 0;
right = _constant;

var identifier = _identifier;
if (identifier is null || context.OperatorOverloadingAllowed)
{
return false;
}

var engine = context.Engine;
if (engine.ExecutionContext.Suspendable is not null)
{
return false;
}

return _slotCache.TryResolve(engine, engine.ExecutionContext.LexicalEnvironment, identifier.Identifier, out var environment, out var slotIndex)
&& environment.TryGetNumberSlot(slotIndex, out left);
}
}

/// <summary>
/// Evaluates both operands with proper suspension checks for async/generator functions.
/// Returns false if evaluation was suspended (caller should return early).
Expand Down Expand Up @@ -344,12 +392,20 @@ public override bool GetBooleanValue(EvaluationContext context)

private sealed class LessBinaryExpression : JintBinaryExpression
{
private NumericConstantComparisonLane _numericLane;

public LessBinaryExpression(NonLogicalBinaryExpression expression) : base(expression)
{
_numericLane.Initialize(_left, _right);
}

protected override object EvaluateInternal(EvaluationContext context)
{
if (_numericLane.TryGetOperands(context, out var unboxedLeft, out var unboxedRight))
{
return unboxedLeft < unboxedRight ? JsBoolean.True : JsBoolean.False;
}

if (!TryEvaluateOperands(context, out var left, out var right))
{
return JsValue.Undefined;
Expand All @@ -368,6 +424,11 @@ protected override object EvaluateInternal(EvaluationContext context)

public override bool GetBooleanValue(EvaluationContext context)
{
if (_numericLane.TryGetOperands(context, out var unboxedLeft, out var unboxedRight))
{
return unboxedLeft < unboxedRight;
}

if (!TryEvaluateOperands(context, out var left, out var right))
{
return false;
Expand All @@ -386,12 +447,20 @@ public override bool GetBooleanValue(EvaluationContext context)

private sealed class GreaterBinaryExpression : JintBinaryExpression
{
private NumericConstantComparisonLane _numericLane;

public GreaterBinaryExpression(NonLogicalBinaryExpression expression) : base(expression)
{
_numericLane.Initialize(_left, _right);
}

protected override object EvaluateInternal(EvaluationContext context)
{
if (_numericLane.TryGetOperands(context, out var unboxedLeft, out var unboxedRight))
{
return unboxedLeft > unboxedRight ? JsBoolean.True : JsBoolean.False;
}

if (!TryEvaluateOperands(context, out var left, out var right))
{
return JsValue.Undefined;
Expand All @@ -410,6 +479,11 @@ protected override object EvaluateInternal(EvaluationContext context)

public override bool GetBooleanValue(EvaluationContext context)
{
if (_numericLane.TryGetOperands(context, out var unboxedLeft, out var unboxedRight))
{
return unboxedLeft > unboxedRight;
}

if (!TryEvaluateOperands(context, out var left, out var right))
{
return false;
Expand Down Expand Up @@ -738,14 +812,24 @@ public override bool GetBooleanValue(EvaluationContext context)
private sealed class CompareBinaryExpression : JintBinaryExpression
{
private readonly bool _leftFirst;
private NumericConstantComparisonLane _numericLane;

public CompareBinaryExpression(NonLogicalBinaryExpression expression, bool leftFirst) : base(expression)
{
_leftFirst = leftFirst;
_numericLane.Initialize(_left, _right);
}

protected override object EvaluateInternal(EvaluationContext context)
{
if (_numericLane.TryGetOperands(context, out var unboxedLeft, out var unboxedRight))
{
// leftFirst == true is `>=`, false is `<=`; NaN yields false either way,
// matching the undefined completion below
var result = _leftFirst ? unboxedLeft >= unboxedRight : unboxedLeft <= unboxedRight;
return result ? JsBoolean.True : JsBoolean.False;
}

if (!TryEvaluateOperands(context, out var leftValue, out var rightValue))
{
return JsValue.Undefined;
Expand All @@ -766,6 +850,11 @@ protected override object EvaluateInternal(EvaluationContext context)

public override bool GetBooleanValue(EvaluationContext context)
{
if (_numericLane.TryGetOperands(context, out var unboxedLeft, out var unboxedRight))
{
return _leftFirst ? unboxedLeft >= unboxedRight : unboxedLeft <= unboxedRight;
}

if (!TryEvaluateOperands(context, out var leftValue, out var rightValue))
{
return false;
Expand Down
Loading