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

namespace Jint.Benchmark;

/// <summary>
/// Recursive call shapes, fresh engine per invocation. Direct-recursive functions otherwise allocate
/// a FunctionEnvironment + Binding[] on every call (the single-slot env pool cannot serve recursion);
/// the bounded recursive env pool lets simultaneously live frames reuse env + slots.
/// - Fib / Tak: wide branching recursion (many sibling calls at bounded depth) — the pool's best case.
/// - DeepSum: linear recursion deeper than the pool cap, repeated — the cap-bounded case.
/// </summary>
[MemoryDiagnoser]
[HideColumns("Error", "StdDev", "Median", "Gen0", "Gen1", "Gen2")]
public class RecursionBenchmark
{
private readonly Prepared<Script> _fib;
private readonly Prepared<Script> _tak;
private readonly Prepared<Script> _deepSum;

public RecursionBenchmark()
{
_fib = Engine.PrepareScript("function fib(n){ return n < 2 ? n : fib(n - 1) + fib(n - 2); } fib(30);");

_tak = Engine.PrepareScript(
"function tak(x, y, z){ return y < x ? tak(tak(x-1,y,z), tak(y-1,z,x), tak(z-1,x,y)) : z; } tak(18, 12, 6);");

_deepSum = Engine.PrepareScript(
"function sum(n){ return n === 0 ? 0 : n + sum(n - 1); } var t = 0; for (var i = 0; i < 2000; i++) { t = sum(400); } t;");
}

[Benchmark]
public void Fib() => new Engine().Evaluate(_fib);

[Benchmark]
public void Tak() => new Engine().Evaluate(_tak);

[Benchmark]
public void DeepSum() => new Engine().Evaluate(_deepSum);
}
40 changes: 26 additions & 14 deletions Jint/Native/Function/ScriptFunction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,11 @@ protected internal override JsValue Call(JsValue thisObject, JsCallArguments arg
Throw.TypeError(calleeContext.Realm, $"Class constructor {_functionDefinition.Name} cannot be invoked without 'new'");
}

// Capture funcEnv for end-of-call pool return when bindings can't escape.
// Skip for direct-recursive functions: the pool's single slot can't help recursion
// (only the topmost frame is reusable), and the Interlocked.Exchange ops in the
// finally block are pure overhead in tight recursive loops.
// Capture funcEnv for end-of-call pool return when bindings can't escape. Direct-recursive
// functions return their env to a small bounded pool (so each live frame reuses a distinct
// env); other non-escaping functions use the single-slot pool. Escaping envs are not pooled.
state = _functionDefinition.Initialize();
if (state is { EnvironmentMayEscape: false, IsDirectRecursive: false })
if (state is { EnvironmentMayEscape: false })
{
funcEnv = (FunctionEnvironment) calleeContext.LexicalEnvironment;
}
Expand Down Expand Up @@ -125,17 +124,30 @@ protected internal override JsValue Call(JsValue thisObject, JsCallArguments arg
{
if (funcEnv is not null)
{
// Cache the slot array for reuse by next call to the same function (thread-safe)
if (funcEnv._slots is { } slots)
if (state!.IsDirectRecursive)
{
System.Array.Clear(slots, 0, slots.Length);
Interlocked.Exchange(ref state!._cachedSlots, slots);
funcEnv._slots = null;
// Return the env (with its fixed-slot array still attached) to the bounded
// recursive pool so another simultaneously live frame can reuse env + slots.
if (funcEnv._slots is { } recursiveSlots)
{
System.Array.Clear(recursiveSlots, 0, recursiveSlots.Length);
}
state.ReturnRecursiveEnv(funcEnv);
}
else
{
// Cache the slot array for reuse by next call to the same function (thread-safe)
if (funcEnv._slots is { } slots)
{
System.Array.Clear(slots, 0, slots.Length);
Interlocked.Exchange(ref state._cachedSlots, slots);
funcEnv._slots = null;
}

// Return the env itself to the per-State pool so the next call to a function
// sharing this State (typically the same Function instance) avoids the allocation.
Interlocked.Exchange(ref state._cachedEnv, funcEnv);
}

// Return the env itself to the per-State pool so the next call to a function
// sharing this State (typically the same Function instance) avoids the allocation.
Interlocked.Exchange(ref state!._cachedEnv, funcEnv);
}
_engine.LeaveExecutionContext();
}
Expand Down
47 changes: 43 additions & 4 deletions Jint/Runtime/Environments/JintEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,15 @@ internal static DeclarativeEnvironment NewDeclarativeEnvironment(Engine engine,
internal static FunctionEnvironment NewFunctionEnvironment(Engine engine, Function f, JsValue newTarget)
{
var state = f._functionDefinition?.Initialize();

// Direct-recursive functions use a small bounded pool so each simultaneously live frame rents a
// distinct env (with its fixed-slot array attached) instead of allocating per call. The single
// _cachedEnv slot below cannot serve recursion — only the topmost frame would ever be reusable.
if (state is { EnvironmentMayEscape: false, IsDirectRecursive: true })
{
return NewRecursiveFunctionEnvironment(engine, f, newTarget, state);
}

FunctionEnvironment env;

// Reuse a pooled FunctionEnvironment when the function's bindings cannot escape the call
Expand Down Expand Up @@ -129,16 +138,46 @@ internal static FunctionEnvironment NewFunctionEnvironment(Engine engine, Functi
if (state is { UseFixedSlots: true })
{
env._slotNames = state.SlotNames;
// Try to reuse cached slots from previous call to same function (thread-safe). Skip
// for direct-recursive functions: the single pool slot is useless for recursion and
// the atomic ops dominate over the saved alloc on tight recursive loops.
var cached = state.IsDirectRecursive ? null : Interlocked.Exchange(ref state._cachedSlots, null);
// Try to reuse cached slots from the previous call to the same function (thread-safe).
// Direct-recursive functions never reach here (handled by NewRecursiveFunctionEnvironment).
var cached = Interlocked.Exchange(ref state._cachedSlots, null);
env._slots = cached ?? new Binding[state.SlotNames!.Length];
}

return env;
}

private static FunctionEnvironment NewRecursiveFunctionEnvironment(
Engine engine,
Function f,
JsValue newTarget,
Interpreter.JintFunctionDefinition.State state)
{
var pooled = state.TryRentRecursiveEnv();
if (pooled is not null && ReferenceEquals(pooled._engine, engine))
{
// The env still carries its cleared fixed-slot Binding[] (and dictionary capacity) from when
// it was pooled; Reset re-binds this/newTarget/outer and clears the dictionary in place. Same
// State means the slot names and slot-array length already match.
pooled.Reset(f, newTarget, f._environment);
return pooled;
}

// Pool empty (or the rented env belonged to a different engine sharing this State — dropped).
var env = new FunctionEnvironment(engine, f, newTarget)
{
_outerEnv = f._environment,
};

if (state.UseFixedSlots)
{
env._slotNames = state.SlotNames;
env._slots = new Binding[state.SlotNames!.Length];
}

return env;
}

/// <summary>
/// https://tc39.es/ecma262/#sec-newglobalenvironment
/// </summary>
Expand Down
64 changes: 64 additions & 0 deletions Jint/Runtime/Interpreter/JintFunctionDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,70 @@ internal sealed class State
public Binding[]? _cachedSlots;
public Environments.FunctionEnvironment? _cachedEnv;

// Bounded best-effort reuse pool for direct-recursive activations. The single _cachedEnv slot
// above can only hold the topmost frame, so recursion otherwise allocates a fresh env+slots per
// call; this lets each of several simultaneously live frames rent a distinct env (with its
// cleared fixed-slot Binding[] still attached). Per-slot Interlocked keeps it safe for the
// parallel fixtures that share a cached State (see state_pool_thread_safety); the count is an
// occupancy hint so the common "pool empty during a deep descent" case skips the scan entirely.
// Bounded so deep recursion cannot retain an unbounded number of environments.
private const int RecursiveEnvPoolSize = 16;
private Environments.FunctionEnvironment?[]? _recursiveEnvPool;
private int _recursiveEnvPoolCount;

internal Environments.FunctionEnvironment? TryRentRecursiveEnv()
{
if (System.Threading.Volatile.Read(ref _recursiveEnvPoolCount) == 0)
{
return null;
}

var pool = _recursiveEnvPool;
if (pool is null)
{
return null;
}

for (var i = 0; i < pool.Length; i++)
{
var env = System.Threading.Interlocked.Exchange(ref pool[i], null);
if (env is not null)
{
System.Threading.Interlocked.Decrement(ref _recursiveEnvPoolCount);
return env;
}
}

return null;
}

internal void ReturnRecursiveEnv(Environments.FunctionEnvironment env)
{
// Drop cheaply when the pool is already full — otherwise a deep recursion (depth >> pool
// size) would scan every slot only to fail on each return during the unwind.
if (System.Threading.Volatile.Read(ref _recursiveEnvPoolCount) >= RecursiveEnvPoolSize)
{
return;
}

var pool = _recursiveEnvPool ?? EnsureRecursiveEnvPool();
for (var i = 0; i < pool.Length; i++)
{
if (System.Threading.Interlocked.CompareExchange(ref pool[i], env, null) is null)
{
System.Threading.Interlocked.Increment(ref _recursiveEnvPoolCount);
return;
}
}
// Pool full: drop this env and let it be collected.
}

private Environments.FunctionEnvironment?[] EnsureRecursiveEnvPool()
{
var created = new Environments.FunctionEnvironment?[RecursiveEnvPoolSize];
return System.Threading.Interlocked.CompareExchange(ref _recursiveEnvPool, created, null) ?? created;
}

public SourceText SourceText;

internal readonly record struct VariableValuePair(Key Name, JsValue? InitialValue);
Expand Down
Loading