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

namespace Jint.Benchmark;

/// <summary>
/// Isolates top-level (global binding) variable access — the stopwatch.js shape where loop
/// counters and state live as global-object properties and every read/write pays a property
/// dictionary lookup. LocalVarLoop is the fixed-slot ceiling/guard for the same operations.
/// </summary>
[MemoryDiagnoser]
[HideColumns("Error", "Gen0", "Gen1", "Gen2")]
public class GlobalAccessBenchmarks
{
private Engine _engine = null!;
private Prepared<Script> _globalVarLoop;
private Prepared<Script> _localVarLoop;

[GlobalSetup]
public void Setup()
{
_globalVarLoop = Engine.PrepareScript("""
gx = 0; gy = 0; gz = 0;
for (var gi = 0; gi < 200000; gi++) {
gz = gx ^ gy;
gx = (gx + 1) & 1023;
gy = (gy + (gz & 3)) & 2047;
}
gz;
""", strict: true);

_localVarLoop = Engine.PrepareScript("""
(function() {
var x = 0, y = 0, z = 0;
for (var i = 0; i < 200000; i++) {
z = x ^ y;
x = (x + 1) & 1023;
y = (y + (z & 3)) & 2047;
}
return z;
})();
""", strict: true);

_engine = new Engine(static options => options.Strict());
_engine.Execute("var gx = 0; var gy = 0; var gz = 0;");
_engine.Evaluate(_globalVarLoop);
_engine.Evaluate(_localVarLoop);
}

[Benchmark]
public JsValue GlobalVarLoop() => _engine.Evaluate(_globalVarLoop);

[Benchmark]
public JsValue LocalVarLoop() => _engine.Evaluate(_localVarLoop);
}
9 changes: 8 additions & 1 deletion Jint/Runtime/Environments/GlobalEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,15 @@ public GlobalDeclarativeEnvironment(Engine engine) : base(engine)
internal readonly ObjectInstance _global;

// we expect it to be GlobalObject, but need to allow to something host-defined, like Window
private readonly GlobalObject? _globalObject;
internal readonly GlobalObject? _globalObject;

// Environment records are needed by debugger
internal readonly GlobalDeclarativeEnvironment _declarativeRecord;

// Bumped whenever the SET of lexical (let/const) declarations changes; lets identifier
// nodes cache resolved global-object bindings and detect later shadowing declarations.
internal int _lexicalMutations;

public GlobalEnvironment(Engine engine, ObjectInstance global) : base(engine)
{
_global = global;
Expand Down Expand Up @@ -124,6 +128,7 @@ internal override void CreateMutableBinding(Key name, bool canBeDeleted = false)
ThrowAlreadyDeclaredException(name);
}

_lexicalMutations++;
_declarativeRecord.CreateMutableBinding(name, canBeDeleted);
}

Expand All @@ -137,6 +142,7 @@ internal override void CreateImmutableBinding(Key name, bool strict = true)
ThrowAlreadyDeclaredException(name);
}

_lexicalMutations++;
_declarativeRecord.CreateImmutableBinding(name, strict);
}

Expand Down Expand Up @@ -249,6 +255,7 @@ internal override bool DeleteBinding(Key name)
{
if (_declarativeRecord.HasBinding(name))
{
_lexicalMutations++;
return _declarativeRecord.DeleteBinding(name);
}

Expand Down
79 changes: 79 additions & 0 deletions Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using Jint.Native;
using Jint.Native.Function;
using Jint.Runtime.Descriptors;
using Jint.Runtime.Environments;

using Environment = Jint.Runtime.Environments.Environment;
Expand Down Expand Up @@ -597,6 +599,19 @@ private JsValue SetValue(EvaluationContext context)
var env = engine.ExecutionContext.LexicalEnvironment;
var strict = StrictModeScope.IsStrictModeCode;
var identifier = left.Identifier;

// Global-binding fast path: write directly through the cached plain writable
// MutableBinding data descriptor. The null pre-check keeps the cost for non-global
// identifiers to a single field test; the rest stays out-of-line.
if (left._cachedGlobalEnv is not null)
{
var cachedGlobalDescriptor = left.TryGetValidatedGlobalDescriptor(engine, env);
if (cachedGlobalDescriptor is not null)
{
return AssignToCachedGlobalBinding(context, left, right, cachedGlobalDescriptor, hasEvalOrArguments, nameAnonymousFunction, strict);
}
}

if (JintEnvironment.TryGetIdentifierEnvironmentWithBinding(
env,
identifier,
Expand Down Expand Up @@ -636,10 +651,74 @@ private JsValue SetValue(EvaluationContext context)
}

environmentRecord.SetMutableBinding(identifier, rval, strict);

// Populate the global-binding cache from the write side too, so write-first
// patterns benefit from the next access on. Must run after the set: the set
// may have created the property (bumping the shape version).
if (ReferenceEquals(environmentRecord, env) && environmentRecord is GlobalEnvironment globalEnv)
{
left.TryRememberGlobalBinding(globalEnv);
}

return rval;
}

return null;
}

/// <summary>
/// The cached-global-binding arm of <see cref="AssignToIdentifier"/>; semantics are
/// identical to its slow path with the final store mirroring
/// GlobalObject.SetFromMutableBinding's writable MutableBinding data-property fast path.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static JsValue AssignToCachedGlobalBinding(
EvaluationContext context,
JintIdentifierExpression left,
JintExpression right,
PropertyDescriptor descriptor,
bool hasEvalOrArguments,
bool nameAnonymousFunction,
bool strict)
{
var engine = context.Engine;
var identifier = left.Identifier;

if (strict && hasEvalOrArguments && identifier.Key != KnownKeys.Eval)
{
Throw.SyntaxError(engine.Realm, "Invalid assignment target");
}

JsValue completion;
if (nameAnonymousFunction && right is JintClassExpression classExpression && right._expression.IsAnonymousFunctionDefinition())
{
completion = classExpression.EvaluateWithName(context, identifier.Value.ToString());
}
else
{
completion = right.GetValue(context);
}

if (context.IsAbrupt())
{
return completion;
}

// If generator suspended or return requested during right-hand side evaluation, don't assign
if (context.IsGeneratorAborted())
{
return completion;
}

var rval = completion.Clone();

if (nameAnonymousFunction && right._expression.IsFunctionDefinition() && right is not JintClassExpression)
{
((Function) rval).SetFunctionName(identifier.Value);
}

descriptor._value = rval;
return rval;
}
}
}
80 changes: 80 additions & 0 deletions Jint/Runtime/Interpreter/Expressions/JintIdentifierExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ internal sealed class JintIdentifierExpression : JintExpression
private DeclarativeEnvironment? _cachedSlotEnv;
private int _cachedSlotIndex = -1;

// Version-based inline cache for global bindings: when top-level code (current lexical
// env IS the global env) resolves to a plain writable MutableBinding data property on the
// real GlobalObject, remember the descriptor. Valid while the global object's own-property
// shape and the set of global lexical declarations are unchanged — value writes mutate the
// descriptor in place and bump neither version. Mirrors JintMemberExpression's read cache.
internal GlobalEnvironment? _cachedGlobalEnv;
private Runtime.Descriptors.PropertyDescriptor? _cachedGlobalDescriptor;
private uint _cachedGlobalShapeVersion;
private int _cachedGlobalLexicalVersion;

// Bounded walk: chain depth is typically 1-3 in real code; deeper chains fall through.
private const int MaxSlotCacheChainDepth = 4;

Expand Down Expand Up @@ -136,6 +146,24 @@ public override JsValue GetValue(EvaluationContext context)
}


// Global-binding fast path: top-level identifier reads hit the cached descriptor
// directly, skipping the property dictionary lookup entirely. The null pre-check keeps
// the cost for non-global identifiers to a single field test; the out-of-line validator
// keeps this method's code size unaffected.
if (_cachedGlobalEnv is not null)
{
var cachedGlobalDescriptor = TryGetValidatedGlobalDescriptor(engine, env);
if (cachedGlobalDescriptor is not null)
{
value = cachedGlobalDescriptor._value;
if (value is null)
{
ThrowNotInitialized(engine);
}
return MaterializeIfArguments(value);
}
}

if (ReferenceEquals(env, _cachedEnvironment)
&& _cachedStrict == strict
&& env.TryGetBinding(identifier, strict, out value))
Expand Down Expand Up @@ -174,6 +202,10 @@ public override JsValue GetValue(EvaluationContext context)
_cachedSlotIndex = slotIndex;
}
}
else if (ReferenceEquals(identifierEnvironment, env) && identifierEnvironment is GlobalEnvironment globalEnv)
{
TryRememberGlobalBinding(globalEnv);
}

if (value is null)
{
Expand All @@ -189,6 +221,54 @@ public override JsValue GetValue(EvaluationContext context)
return MaterializeIfArguments(value);
}

/// <summary>
/// Validates the global-binding cache for the current environment: the current lexical env
/// must be the cached GlobalEnvironment itself (top-level code) and neither the global
/// object's own-property shape nor the global lexical declaration set may have changed.
/// Returns the cached plain writable data descriptor, or null on miss.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
internal Runtime.Descriptors.PropertyDescriptor? TryGetValidatedGlobalDescriptor(Engine engine, Environment env)
{
var cachedGlobalEnv = _cachedGlobalEnv;
if (cachedGlobalEnv is not null
&& ReferenceEquals(env, cachedGlobalEnv)
&& ReferenceEquals(cachedGlobalEnv._engine, engine)
&& cachedGlobalEnv._global._propertiesVersion == _cachedGlobalShapeVersion
&& cachedGlobalEnv._lexicalMutations == _cachedGlobalLexicalVersion)
{
return _cachedGlobalDescriptor;
}

return null;
}

/// <summary>
/// Caches the resolved global binding when it is a plain writable MutableBinding data
/// property on the real GlobalObject and no lexical declaration shadows it. Both reads
/// and writes may then operate on the descriptor directly while the versions hold.
/// </summary>
internal void TryRememberGlobalBinding(GlobalEnvironment globalEnv)
{
var identifier = _identifier;
if (globalEnv._globalObject is not { } globalObject
|| globalEnv._declarativeRecord.HasBinding(identifier.Key))
{
return;
}

if (globalObject._properties!.TryGetValue(identifier.Key, out var descriptor)
&& descriptor.IsDataDescriptor()
&& descriptor.Writable
&& (descriptor._flags & Runtime.Descriptors.PropertyFlag.MutableBinding) != Runtime.Descriptors.PropertyFlag.None)
{
_cachedGlobalEnv = globalEnv;
_cachedGlobalDescriptor = descriptor;
_cachedGlobalShapeVersion = globalObject._propertiesVersion;
_cachedGlobalLexicalVersion = globalEnv._lexicalMutations;
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static JsValue MaterializeIfArguments(JsValue value)
{
Expand Down
Loading