From 02c1aa98ea7d78510e071672a4b3da1d4dff5a08 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Thu, 30 Jul 2026 10:45:11 +0300 Subject: [PATCH 1/2] Ask once per loop, not once per element, how to call a built-in's callback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Array.prototype.map, forEach, filter, every, reduce and their typed-array, Map, Set and Array.from siblings all rent one JsValue[] outside the loop, rewrite its slots per element and invoke through ICallable.Call. For an interpreted callback the callee then reads those same slots straight back out into its fixed parameter slots, so the array is a round trip that the register lane can skip entirely. CallbackInvoker is where the question is asked. The callback does not change across elements, so neither can the answer: State is computed once and cached on the immutable AST node, _isClassConstructor is decided when a class is defined, and Engine._isDebugMode is readonly. Building the invoker resolves the whole gate once, before the loop, and every element after that is one null check against the resolved target — never a re-probe. The two lanes share one shape rather than one loop body being written twice. Rent() hands back an invoker that took the register lane and rented nothing, or one holding the pooled array the site would have rented anyway; Call() either fills registers or writes the leading slots and dispatches through ICallable exactly as before. Create() is the same for a caller that outlives a single built-in call — a sort comparer, which is the highest-frequency callback here at n log n invocations — and allocates its array only if the register lane declined. Callbacks in this family almost all end in the collection itself, the same value on every call, so the factory takes it separately: the array lane keeps its single hoisted store and the register lane passes it from a field, which is why converting a site adds no per-element work to the path it did not take. Equivalence is structural rather than argued. ScriptFunction.Call already routes a SupportsRegisterCall callee into CallCore with an ArrayArguments, so for exactly the callees this lane accepts the conversion swaps the argument source and changes nothing else — not the call-stack frame (built-in callback invocation pushes none either way), not thisArg, not Arguments.At semantics for a callback declaring more parameters than the site supplies. Sites that deliberately leak their rented array on an early return still do; sites with a try/finally still return it there. IteratorProtocol.Execute is left alone: its argument count is a constructor parameter and the rented array is handed to an abstract ProcessItem that three of its four overrides ignore, so routing it would mean changing the protocol's signature for one real callback. Co-Authored-By: Claude Opus 5 (1M context) --- Jint/Native/Array/ArrayConstructor.cs | 24 +-- Jint/Native/Array/ArrayInstance.cs | 37 +--- Jint/Native/Array/ArrayPrototype.cs | 121 ++++------- Jint/Native/CallbackInvoker.cs | 194 ++++++++++++++++++ Jint/Native/GroupByHelper.cs | 12 +- Jint/Native/Iterator/IteratorProtocol.cs | 9 +- Jint/Native/JsMap.cs | 9 +- Jint/Native/JsSet.cs | 9 +- Jint/Native/Object/ObjectInstance.cs | 15 +- .../IntrinsicTypedArrayConstructor.cs | 6 +- .../IntrinsicTypedArrayPrototype.cs | 108 +++------- 11 files changed, 295 insertions(+), 249 deletions(-) create mode 100644 Jint/Native/CallbackInvoker.cs diff --git a/Jint/Native/Array/ArrayConstructor.cs b/Jint/Native/Array/ArrayConstructor.cs index 99ac96c19d..9d3c83bcc6 100644 --- a/Jint/Native/Array/ArrayConstructor.cs +++ b/Jint/Native/Array/ArrayConstructor.cs @@ -95,7 +95,7 @@ private JsArray ConstructFromIterator(JsValue items, ICallable usingIterator, IC var iterator = items.GetIterator(_realm, method: usingIterator); var builder = new JsValueListBuilder(16); - var args = callable is not null ? _engine._jsValueArrayPool.RentArray(2) : null; + var invoker = callable is not null ? CallbackInvoker.Rent(_engine, callable, 2) : default; try { var iterations = 0; @@ -119,9 +119,7 @@ private JsArray ConstructFromIterator(JsValue items, ICallable usingIterator, IC if (callable is not null) { - args![0] = jsValue; - args[1] = index; - jsValue = callable.Call(thisArg, args); + jsValue = invoker.Call(thisArg, jsValue, index); } builder.Add(jsValue); @@ -138,10 +136,7 @@ private JsArray ConstructFromIterator(JsValue items, ICallable usingIterator, IC finally { builder.Dispose(); - if (args is not null) - { - _engine._jsValueArrayPool.ReturnArray(args); - } + invoker.Return(); } } @@ -665,9 +660,7 @@ private ObjectInstance ConstructArrayFromArrayLike( a = ArrayCreate(length); } - var args = callable is not null - ? _engine._jsValueArrayPool.RentArray(2) - : null; + var invoker = callable is not null ? CallbackInvoker.Rent(_engine, callable, 2) : default; var target = ArrayOperations.For(a, forWrite: true); uint n = 0; @@ -682,9 +675,7 @@ private ObjectInstance ConstructArrayFromArrayLike( var value = source.Get(i); if (callable is not null) { - args![0] = value; - args[1] = i; - value = callable.Call(thisArg, args); + value = invoker.Call(thisArg, value, i); // function can alter data length = source.GetLength(); @@ -694,10 +685,7 @@ private ObjectInstance ConstructArrayFromArrayLike( n++; } - if (callable is not null) - { - _engine._jsValueArrayPool.ReturnArray(args!); - } + invoker.Return(); target.SetLength(length); return a; diff --git a/Jint/Native/Array/ArrayInstance.cs b/Jint/Native/Array/ArrayInstance.cs index f3bed877e5..7b98f9dcaf 100644 --- a/Jint/Native/Array/ArrayInstance.cs +++ b/Jint/Native/Array/ArrayInstance.cs @@ -1667,17 +1667,12 @@ internal JsArray Map(JsCallArguments arguments) var callable = GetCallable(callbackfn); var a = _engine.Realm.Intrinsics.Array.ArrayCreate(len); - // args is rented from the pool whose factory allocates new JsValue[3], so it is an exact - // JsValue[]; the per-element fills below bypass the covariant array store type check. - var args = _engine._jsValueArrayPool.RentArray(3); - Arguments.WriteNoTypeCheck(args, 2, this); + var invoker = CallbackInvoker.Rent(_engine, callable, 3, this); for (uint k = 0; k < len; k++) { if (TryGetValue(k, out var kvalue)) { - Arguments.WriteNoTypeCheck(args, 0, kvalue); - Arguments.WriteNoTypeCheck(args, 1, k); - var mappedValue = callable.Call(thisArg, args); + var mappedValue = invoker.Call(thisArg, kvalue, k); if (a._dense != null && k < (uint) a._dense.Length) { a._dense[k] = mappedValue; @@ -1689,7 +1684,7 @@ internal JsArray Map(JsCallArguments arguments) } } - _engine._jsValueArrayPool.ReturnArray(args); + invoker.Return(); return a; } @@ -1706,10 +1701,7 @@ internal JsArray Filter(JsCallArguments arguments) // materialize an exact-size result instead of growing the result's dense backing by // doubling. Capped initial rent so a large low-selectivity source doesn't rent ~len slots. var builder = new JsValueListBuilder((int) System.Math.Min(len, 1024)); - // args is rented from the pool whose factory allocates new JsValue[3], so it is an exact - // JsValue[]; the per-element fills below bypass the covariant array store type check. - var args = _engine._jsValueArrayPool.RentArray(3); - Arguments.WriteNoTypeCheck(args, 2, this); + var invoker = CallbackInvoker.Rent(_engine, callable, 3, this); // try/finally so the rented args array and the pooled builder buffer are released // when the callback or a periodic Check() throws. @@ -1724,9 +1716,7 @@ internal JsArray Filter(JsCallArguments arguments) if (TryGetValue(k, out var kvalue)) { - Arguments.WriteNoTypeCheck(args, 0, kvalue); - Arguments.WriteNoTypeCheck(args, 1, k); - var selected = callable.Call(thisArg, args); + var selected = invoker.Call(thisArg, kvalue, k); if (TypeConverter.ToBoolean(selected)) { builder.Add(kvalue); @@ -1739,7 +1729,7 @@ internal JsArray Filter(JsCallArguments arguments) finally { builder.Dispose(); - _engine._jsValueArrayPool.ReturnArray(args); + invoker.Return(); } } @@ -1762,10 +1752,7 @@ internal sealed override bool FindWithCallback( return false; } - // args is rented from the pool whose factory allocates new JsValue[3], so it is an exact - // JsValue[]; the per-element fills below bypass the covariant array store type check. - var args = _engine._jsValueArrayPool.RentArray(3); - Arguments.WriteNoTypeCheck(args, 2, this); + var invoker = CallbackInvoker.Rent(_engine, callable, 3, this); // try/finally so the rented pool array is returned on every exit path: the early // return-on-match below and a periodic Check() throw both previously leaked it. @@ -1783,9 +1770,7 @@ internal sealed override bool FindWithCallback( if (TryGetValue(k, out var kvalue) || visitUnassigned) { kvalue ??= Undefined; - Arguments.WriteNoTypeCheck(args, 0, kvalue); - Arguments.WriteNoTypeCheck(args, 1, k); - var testResult = callable.Call(thisArg, args); + var testResult = invoker.Call(thisArg, kvalue, k); if (TypeConverter.ToBoolean(testResult)) { index = k; @@ -1808,9 +1793,7 @@ internal sealed override bool FindWithCallback( if (TryGetValue(idx, out var kvalue) || visitUnassigned) { kvalue ??= Undefined; - Arguments.WriteNoTypeCheck(args, 0, kvalue); - Arguments.WriteNoTypeCheck(args, 1, idx); - var testResult = callable.Call(thisArg, args); + var testResult = invoker.Call(thisArg, kvalue, idx); if (TypeConverter.ToBoolean(testResult)) { index = idx; @@ -1823,7 +1806,7 @@ internal sealed override bool FindWithCallback( } finally { - _engine._jsValueArrayPool.ReturnArray(args); + invoker.Return(); } index = 0; diff --git a/Jint/Native/Array/ArrayPrototype.cs b/Jint/Native/Array/ArrayPrototype.cs index b5cfdb57db..0ceb789df8 100644 --- a/Jint/Native/Array/ArrayPrototype.cs +++ b/Jint/Native/Array/ArrayPrototype.cs @@ -520,10 +520,7 @@ private JsValue Reduce(JsValue thisObject, JsCallArguments arguments) } } - // args is a freshly allocated exact JsValue[4], so the per-element fills below bypass the - // covariant array store type check. - var args = new JsValue[4]; - Arguments.WriteNoTypeCheck(args, 3, o.Target); + var invoker = CallbackInvoker.Create(_engine, callable, 4, o.Target); while (k < len) { if (k > 0 && k % ConstraintCheckInterval == 0) @@ -534,10 +531,7 @@ private JsValue Reduce(JsValue thisObject, JsCallArguments arguments) var i = (uint) k; if (o.TryGetValue(i, out var kvalue)) { - Arguments.WriteNoTypeCheck(args, 0, accumulator); - Arguments.WriteNoTypeCheck(args, 1, kvalue); - Arguments.WriteNoTypeCheck(args, 2, i); - accumulator = callable.Call(Undefined, args); + accumulator = invoker.Call(Undefined, accumulator, kvalue, i); } k++; @@ -570,10 +564,7 @@ private JsValue Filter(JsValue thisObject, JsCallArguments arguments) var operations = ArrayOperations.For(a, forWrite: true); uint to = 0; - // args is rented from the pool whose factory allocates new JsValue[3], so it is an exact - // JsValue[]; the per-element fills below bypass the covariant array store type check. - var args = _engine._jsValueArrayPool.RentArray(3); - Arguments.WriteNoTypeCheck(args, 2, o.Target); + var invoker = CallbackInvoker.Rent(_engine, callable, 3, o.Target); for (uint k = 0; k < len; k++) { if (k > 0 && k % ConstraintCheckInterval == 0) @@ -583,9 +574,7 @@ private JsValue Filter(JsValue thisObject, JsCallArguments arguments) if (o.TryGetValue(k, out var kvalue)) { - Arguments.WriteNoTypeCheck(args, 0, kvalue); - Arguments.WriteNoTypeCheck(args, 1, k); - var selected = callable.Call(thisArg, args); + var selected = invoker.Call(thisArg, kvalue, k); if (TypeConverter.ToBoolean(selected)) { operations.CreateDataPropertyOrThrow(to, kvalue); @@ -595,7 +584,7 @@ private JsValue Filter(JsValue thisObject, JsCallArguments arguments) } operations.SetLength(to); - _engine._jsValueArrayPool.ReturnArray(args); + invoker.Return(); return a; } @@ -625,10 +614,7 @@ private JsValue Map(JsValue thisObject, JsCallArguments arguments) var callable = GetCallable(callbackfn); var a = ArrayOperations.For(_realm.Intrinsics.Array.ArraySpeciesCreate(TypeConverter.ToObject(_realm, thisObject), (uint) len), forWrite: true); - // args is rented from the pool whose factory allocates new JsValue[3], so it is an exact - // JsValue[]; the per-element fills below bypass the covariant array store type check. - var args = _engine._jsValueArrayPool.RentArray(3); - Arguments.WriteNoTypeCheck(args, 2, o.Target); + var invoker = CallbackInvoker.Rent(_engine, callable, 3, o.Target); for (uint k = 0; k < len; k++) { if (k > 0 && k % ConstraintCheckInterval == 0) @@ -638,13 +624,11 @@ private JsValue Map(JsValue thisObject, JsCallArguments arguments) if (o.TryGetValue(k, out var kvalue)) { - Arguments.WriteNoTypeCheck(args, 0, kvalue); - Arguments.WriteNoTypeCheck(args, 1, k); - var mappedValue = callable.Call(thisArg, args); + var mappedValue = invoker.Call(thisArg, kvalue, k); a.CreateDataPropertyOrThrow(k, mappedValue); } } - _engine._jsValueArrayPool.ReturnArray(args); + invoker.Return(); return a.Target; } @@ -742,14 +726,9 @@ private ulong FlattenIntoArray( var targetIndex = start; ulong sourceIndex = 0; - var callArguments = System.Array.Empty(); - if (mapperFunction is not null) - { - // callArguments is rented from the pool whose factory allocates new JsValue[3], so it is an - // exact JsValue[]; the per-element fills below bypass the covariant array store type check. - callArguments = _engine._jsValueArrayPool.RentArray(3); - Arguments.WriteNoTypeCheck(callArguments, 2, source.Target); - } + var invoker = mapperFunction is not null + ? CallbackInvoker.Rent(_engine, mapperFunction, 3, source.Target) + : default; while (sourceIndex < sourceLen) { @@ -764,9 +743,7 @@ private ulong FlattenIntoArray( var element = source.Get(sourceIndex); if (mapperFunction is not null) { - Arguments.WriteNoTypeCheck(callArguments, 0, element); - Arguments.WriteNoTypeCheck(callArguments, 1, JsNumber.Create(sourceIndex)); - element = mapperFunction.Call(thisArg ?? Undefined, callArguments); + element = invoker.Call(thisArg ?? Undefined, element, JsNumber.Create(sourceIndex)); } var shouldFlatten = false; @@ -800,10 +777,7 @@ private ulong FlattenIntoArray( sourceIndex++; } - if (mapperFunction is not null) - { - _engine._jsValueArrayPool.ReturnArray(callArguments); - } + invoker.Return(); return targetIndex; } @@ -825,14 +799,9 @@ private void FlattenIntoArrayDense( { ulong sourceIndex = 0; - var callArguments = System.Array.Empty(); - if (mapperFunction is not null) - { - // callArguments is rented from the pool whose factory allocates new JsValue[3], so it is an - // exact JsValue[]; the per-element fills below bypass the covariant array store type check. - callArguments = _engine._jsValueArrayPool.RentArray(3); - Arguments.WriteNoTypeCheck(callArguments, 2, source.Target); - } + var invoker = mapperFunction is not null + ? CallbackInvoker.Rent(_engine, mapperFunction, 3, source.Target) + : default; try { @@ -849,9 +818,7 @@ private void FlattenIntoArrayDense( var element = source.Get(sourceIndex); if (mapperFunction is not null) { - Arguments.WriteNoTypeCheck(callArguments, 0, element); - Arguments.WriteNoTypeCheck(callArguments, 1, JsNumber.Create(sourceIndex)); - element = mapperFunction.Call(thisArg ?? Undefined, callArguments); + element = invoker.Call(thisArg ?? Undefined, element, JsNumber.Create(sourceIndex)); } var shouldFlatten = false; @@ -881,10 +848,7 @@ private void FlattenIntoArrayDense( } finally { - if (mapperFunction is not null) - { - _engine._jsValueArrayPool.ReturnArray(callArguments); - } + invoker.Return(); } } @@ -899,10 +863,7 @@ private JsValue ForEach(JsValue thisObject, JsValue arg0, JsValue arg1) var callable = GetCallable(callbackfn); - // args is rented from the pool whose factory allocates new JsValue[3], so it is an exact - // JsValue[]; the per-element fills below bypass the covariant array store type check. - var args = _engine._jsValueArrayPool.RentArray(3); - Arguments.WriteNoTypeCheck(args, 2, o.Target); + var invoker = CallbackInvoker.Rent(_engine, callable, 3, o.Target); for (uint k = 0; k < len; k++) { if (k > 0 && k % ConstraintCheckInterval == 0) @@ -912,12 +873,10 @@ private JsValue ForEach(JsValue thisObject, JsValue arg0, JsValue arg1) if (o.TryGetValue(k, out var kvalue)) { - Arguments.WriteNoTypeCheck(args, 0, kvalue); - Arguments.WriteNoTypeCheck(args, 1, k); - callable.Call(thisArg, args); + invoker.Call(thisArg, kvalue, k); } } - _engine._jsValueArrayPool.ReturnArray(args); + invoker.Return(); return Undefined; } @@ -1037,10 +996,7 @@ private JsValue Every(JsValue thisObject, JsValue arg0, JsValue arg1) return JsBoolean.True; } - // args is rented from the pool whose factory allocates new JsValue[3], so it is an exact - // JsValue[]; the per-element fills below bypass the covariant array store type check. - var args = _engine._jsValueArrayPool.RentArray(3); - Arguments.WriteNoTypeCheck(args, 2, o.Target); + var invoker = CallbackInvoker.Rent(_engine, callable, 3, o.Target); for (uint k = 0; k < len; k++) { if (k > 0 && k % ConstraintCheckInterval == 0) @@ -1050,17 +1006,15 @@ private JsValue Every(JsValue thisObject, JsValue arg0, JsValue arg1) if (o.TryGetValue(k, out var kvalue)) { - Arguments.WriteNoTypeCheck(args, 0, kvalue); - Arguments.WriteNoTypeCheck(args, 1, k); - var testResult = callable.Call(thisArg, args); + var testResult = invoker.Call(thisArg, kvalue, k); if (!TypeConverter.ToBoolean(testResult)) { - _engine._jsValueArrayPool.ReturnArray(args); + invoker.Return(); return JsBoolean.False; } } } - _engine._jsValueArrayPool.ReturnArray(args); + invoker.Return(); return JsBoolean.True; } @@ -2517,10 +2471,7 @@ private JsValue ReduceRight(JsValue thisObject, JsCallArguments arguments) } } - // jsValues is a freshly allocated exact JsValue[4], so the per-element fills below bypass the - // covariant array store type check. - var jsValues = new JsValue[4]; - Arguments.WriteNoTypeCheck(jsValues, 3, o.Target); + var invoker = CallbackInvoker.Create(_engine, callable, 4, o.Target); for (; k >= 0; k--) { if (k % ConstraintCheckInterval == 0) @@ -2530,10 +2481,7 @@ private JsValue ReduceRight(JsValue thisObject, JsCallArguments arguments) if (o.TryGetValue((ulong) k, out var kvalue)) { - Arguments.WriteNoTypeCheck(jsValues, 0, accumulator); - Arguments.WriteNoTypeCheck(jsValues, 1, kvalue); - Arguments.WriteNoTypeCheck(jsValues, 2, k); - accumulator = callable.Call(Undefined, jsValues); + accumulator = invoker.Call(Undefined, accumulator, kvalue, k); } } @@ -2627,12 +2575,18 @@ public static ArrayComparer WithFunction(Engine engine, ICallable? compare) private readonly Engine? _engine; private readonly ICallable? _compare; - private readonly JsValue[] _comparableArray = new JsValue[2]; + // Built once per sort rather than per comparison: the comparator cannot change between two + // comparisons, so neither can the lane it is dispatched through. + private readonly CallbackInvoker _invoker; private ArrayComparer(Engine? engine, ICallable? compare) { _engine = engine; _compare = compare; + if (compare is not null) + { + _invoker = CallbackInvoker.Create(engine!, compare, 2); + } } public int Compare(JsValue? x, JsValue? y) @@ -2678,12 +2632,7 @@ public int Compare(JsValue? x, JsValue? y) { _engine!.RunBeforeExecuteStatementChecks(null); - // _comparableArray is an exact JsValue[2]; bypass the per-comparison covariance check - // (stelem.ref -> CastHelpers.StelemRef) a plain store pays because JsValue is not sealed. - Arguments.WriteNoTypeCheck(_comparableArray, 0, x!); - Arguments.WriteNoTypeCheck(_comparableArray, 1, y!); - - var s = TypeConverter.ToNumber(_compare.Call(Undefined, _comparableArray)); + var s = TypeConverter.ToNumber(_invoker.Call(Undefined, x!, y!)); if (s < 0) { return -1; diff --git a/Jint/Native/CallbackInvoker.cs b/Jint/Native/CallbackInvoker.cs new file mode 100644 index 0000000000..512687327b --- /dev/null +++ b/Jint/Native/CallbackInvoker.cs @@ -0,0 +1,194 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Jint.Native.Function; +using Jint.Pooling; +using Jint.Runtime; +using Jint.Runtime.Interpreter; + +namespace Jint.Native; + +/// +/// The user callback a built-in invokes once per element, with the decision of HOW to invoke it +/// taken once — when the invoker is built, outside the loop — instead of per element. +/// +/// +/// +/// The array lane is exactly what these built-ins did before: one argument array, rented from the +/// pool (or allocated) once, its slots rewritten per element, invoked through +/// . The register lane hands the same values over in locals through +/// , so an interpreted callback's fixed parameter +/// slots are filled straight from registers — no array stores on the way in, no reads back out, +/// and no array rented at all. +/// +/// +/// The eligibility question — is this a plain interpreted function whose instantiation is the +/// fixed-slot fast path — depends only on the callback, which does not change across elements, so +/// it is asked once. Every input to the answer is fixed for the callback's lifetime: +/// is computed once and cached on the immutable AST +/// node, _isClassConstructor is decided when a class is defined, and +/// Engine._isDebugMode is readonly. A callback that mutates the collection mid-iteration, +/// throws, or is a revoked proxy therefore cannot invalidate the verdict. +/// +/// +/// Most of these callbacks end in the collection itself — the (value, index, target) shape — +/// which is the same value on every call. Handing it to the factory as lastArgument keeps +/// the array lane's hoisted store: only the leading arguments are written per element, exactly as +/// before, and the register lane passes it from a field. +/// +/// +[StructLayout(LayoutKind.Auto)] +internal readonly struct CallbackInvoker +{ + private readonly ICallable _callable; + + // Non-null together, and only when the register lane took: the callee and the State its own + // [[Call]] would have looked up anyway. + private readonly ScriptFunction? _registerTarget; + private readonly JintFunctionDefinition.State? _registerState; + + // Non-null exactly when Return() has something to give back — so a default instance, a + // register-lane invoker and a caller-allocated one are all safe to Return(). + private readonly JsValueArrayPool? _pool; + + // Empty in the register lane, which never materializes an argument array. + private readonly JsValue[] _arguments; + + private readonly JsValue _lastArgument; + private readonly bool _hasLastArgument; + private readonly int _argumentCount; + + private CallbackInvoker(Engine engine, ICallable callable, int argumentCount, JsValue? lastArgument, bool pooled) + { + Debug.Assert(argumentCount is > 0 and <= 4, "the register lane serves at most four arguments"); + + _callable = callable; + _argumentCount = argumentCount; + _hasLastArgument = lastArgument is not null; + _lastArgument = lastArgument ?? JsValue.Undefined; + + // Same gate ScriptFunction.Call applies before taking its own register-backed arm, asked + // here once for the whole loop. Initialize() is what the callee's [[Call]] does first + // anyway, and the State it returns lives on the AST node, so the reference stays valid. + if (!engine._isDebugMode + && callable is ScriptFunction { _isClassConstructor: false } scriptFunction + && scriptFunction._functionDefinition!.Initialize() is { SupportsRegisterCall: true } state) + { + _registerTarget = scriptFunction; + _registerState = state; + _pool = null; + _arguments = []; + return; + } + + _registerTarget = null; + _registerState = null; + + if (pooled) + { + _pool = engine._jsValueArrayPool; + // The pool's factories allocate exactly JsValue[n], so the fills below may bypass the + // covariant array store type check. + _arguments = _pool.RentArray(argumentCount); + } + else + { + _pool = null; + _arguments = new JsValue[argumentCount]; + } + + if (_hasLastArgument) + { + Arguments.WriteNoTypeCheck(_arguments, argumentCount - 1, _lastArgument); + } + } + + /// + /// An invoker whose argument array — if it needs one at all — comes from the engine's pool. + /// The caller must it on the paths where it returns the array today. + /// + /// + /// lastArgument is the callback's final argument when it is the same value on every call + /// (the collection, for the (value, index, target) shape), and null when every + /// argument varies. When supplied it occupies the last of argumentCount positions, so + /// the caller passes one fewer argument to and its + /// siblings. + /// + public static CallbackInvoker Rent(Engine engine, ICallable callable, int argumentCount, JsValue? lastArgument = null) + => new(engine, callable, argumentCount, lastArgument, pooled: true); + + /// + /// An invoker that allocates its own argument array — and only if it turns out to need one — + /// for callers holding it longer than a single built-in call, such as a sort comparer living + /// for the whole sort. Nothing to . + /// + public static CallbackInvoker Create(Engine engine, ICallable callable, int argumentCount, JsValue? lastArgument = null) + => new(engine, callable, argumentCount, lastArgument, pooled: false); + + /// + /// Invokes the callback with one leading argument, plus the fixed last argument if one was + /// supplied to the factory. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public JsValue Call(JsValue thisArgument, JsValue argument0) + { + Debug.Assert(_argumentCount == (_hasLastArgument ? 2 : 1), "leading arguments plus the fixed one must be the callback's arity"); + + if (_registerTarget is not null) + { + return _registerTarget.CallFromRegisters(thisArgument, argument0, _lastArgument, JsValue.Undefined, JsValue.Undefined, _argumentCount, _registerState!); + } + + var arguments = _arguments; + Arguments.WriteNoTypeCheck(arguments, 0, argument0); + return _callable.Call(thisArgument, arguments); + } + + /// + /// Invokes the callback with two leading arguments, plus the fixed last argument if one was + /// supplied to the factory. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public JsValue Call(JsValue thisArgument, JsValue argument0, JsValue argument1) + { + Debug.Assert(_argumentCount == (_hasLastArgument ? 3 : 2), "leading arguments plus the fixed one must be the callback's arity"); + + if (_registerTarget is not null) + { + return _registerTarget.CallFromRegisters(thisArgument, argument0, argument1, _lastArgument, JsValue.Undefined, _argumentCount, _registerState!); + } + + var arguments = _arguments; + Arguments.WriteNoTypeCheck(arguments, 0, argument0); + Arguments.WriteNoTypeCheck(arguments, 1, argument1); + return _callable.Call(thisArgument, arguments); + } + + /// + /// Invokes the callback with three leading arguments, plus the fixed last argument if one was + /// supplied to the factory — the reduce shape. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public JsValue Call(JsValue thisArgument, JsValue argument0, JsValue argument1, JsValue argument2) + { + Debug.Assert(_argumentCount == (_hasLastArgument ? 4 : 3), "leading arguments plus the fixed one must be the callback's arity"); + + if (_registerTarget is not null) + { + return _registerTarget.CallFromRegisters(thisArgument, argument0, argument1, argument2, _lastArgument, _argumentCount, _registerState!); + } + + var arguments = _arguments; + Arguments.WriteNoTypeCheck(arguments, 0, argument0); + Arguments.WriteNoTypeCheck(arguments, 1, argument1); + Arguments.WriteNoTypeCheck(arguments, 2, argument2); + return _callable.Call(thisArgument, arguments); + } + + /// + /// Gives a pooled argument array back. A no-op for an invoker that rented none — the register + /// lane, a d one, and a default instance — so callers keep whatever + /// conditional structure they had around the rent. + /// + public void Return() => _pool?.ReturnArray(_arguments); +} diff --git a/Jint/Native/GroupByHelper.cs b/Jint/Native/GroupByHelper.cs index 07b23a579a..c602932ff9 100644 --- a/Jint/Native/GroupByHelper.cs +++ b/Jint/Native/GroupByHelper.cs @@ -24,10 +24,9 @@ private sealed class GroupByProtocol : IteratorProtocol { private readonly Engine _engine; private readonly Dictionary _result; - private readonly ICallable _callable; private readonly bool _mapMode; private ulong _k; - private readonly JsValue[] _callArgs = new JsValue[2]; + private readonly CallbackInvoker _invoker; public GroupByProtocol( Engine engine, @@ -38,8 +37,8 @@ public GroupByProtocol( { _engine = engine; _result = result; - _callable = callable; _mapMode = mapMode; + _invoker = CallbackInvoker.Create(engine, callable, 2); } protected override void ProcessItem(JsValue[] arguments, JsValue currentValue) @@ -49,12 +48,7 @@ protected override void ProcessItem(JsValue[] arguments, JsValue currentValue) Throw.TypeError(_engine.Realm); } - // _callArgs is an exact JsValue[2]; bypass the per-element covariance check - // (stelem.ref -> CastHelpers.StelemRef) a plain store pays because JsValue is not sealed. - Arguments.WriteNoTypeCheck(_callArgs, 0, currentValue); - Arguments.WriteNoTypeCheck(_callArgs, 1, _k); - - var value = _callable.Call(JsValue.Undefined, _callArgs); + var value = _invoker.Call(JsValue.Undefined, currentValue, _k); JsValue key; if (_mapMode) { diff --git a/Jint/Native/Iterator/IteratorProtocol.cs b/Jint/Native/Iterator/IteratorProtocol.cs index 2364882f93..2c42b709c4 100644 --- a/Jint/Native/Iterator/IteratorProtocol.cs +++ b/Jint/Native/Iterator/IteratorProtocol.cs @@ -84,7 +84,7 @@ internal static void AddEntriesFromIterable(ObjectInstance target, IteratorInsta Throw.TypeError(target.Engine.Realm, "adder must be callable"); } - var args = target.Engine._jsValueArrayPool.RentArray(2); + var invoker = CallbackInvoker.Rent(target.Engine, callable, 2); var skipClose = true; var iterations = 0; @@ -116,10 +116,7 @@ internal static void AddEntriesFromIterable(ObjectInstance target, IteratorInsta var k = oi.Get(JsString.NumberZeroString); var v = oi.Get(JsString.NumberOneString); - args[0] = k; - args[1] = v; - - callable.Call(target, args); + invoker.Call(target, k, v); } while (true); } catch @@ -132,7 +129,7 @@ internal static void AddEntriesFromIterable(ObjectInstance target, IteratorInsta } finally { - target.Engine._jsValueArrayPool.ReturnArray(args); + invoker.Return(); } } } diff --git a/Jint/Native/JsMap.cs b/Jint/Native/JsMap.cs index 5ae62906d5..ac61c0c6ae 100644 --- a/Jint/Native/JsMap.cs +++ b/Jint/Native/JsMap.cs @@ -95,8 +95,7 @@ internal JsValue GetOrInsertComputed(JsValue key, ICallable callbackfn) internal void ForEach(ICallable callable, JsValue thisArg) { - var args = _engine._jsValueArrayPool.RentArray(3); - args[2] = this; + var invoker = CallbackInvoker.Rent(_engine, callable, 3, this); var i = 0; var iterations = 0; @@ -109,9 +108,7 @@ internal void ForEach(ICallable callable, JsValue thisArg) } var key = _map.GetKey(i); - args[0] = _map[key]; - args[1] = key; - callable.Call(thisArg, args); + invoker.Call(thisArg, _map[key], key); // Adjust position for mutations during callback if (i < _map.Count && ReferenceEquals(_map.GetKey(i), key)) @@ -132,7 +129,7 @@ internal void ForEach(ICallable callable, JsValue thisArg) // else: key was deleted, entries shifted left so i now points to next entry } - _engine._jsValueArrayPool.ReturnArray(args); + invoker.Return(); } internal ObjectInstance Iterator() => _realm.Intrinsics.MapIteratorPrototype.ConstructEntryIterator(this); diff --git a/Jint/Native/JsSet.cs b/Jint/Native/JsSet.cs index f7948c691e..c92cd2358f 100644 --- a/Jint/Native/JsSet.cs +++ b/Jint/Native/JsSet.cs @@ -58,8 +58,7 @@ protected override bool TryGetProperty(JsValue property, [NotNullWhen(true)] out internal void ForEach(ICallable callable, JsValue thisArg) { - var args = _engine._jsValueArrayPool.RentArray(3); - args[2] = this; + var invoker = CallbackInvoker.Rent(_engine, callable, 3, this); var i = 0; var iterations = 0; @@ -72,9 +71,7 @@ internal void ForEach(ICallable callable, JsValue thisArg) } var value = _set._list[i]; - args[0] = value; - args[1] = value; - callable.Call(thisArg, args); + invoker.Call(thisArg, value, value); // Adjust position for mutations during callback if (i < _set._list.Count && (ReferenceEquals(_set._list[i], value) || SameValueZeroComparer.Equals(_set._list[i], value))) @@ -95,7 +92,7 @@ internal void ForEach(ICallable callable, JsValue thisArg) // else: value was deleted, entries shifted left so i now points to next entry } - _engine._jsValueArrayPool.ReturnArray(args); + invoker.Return(); } internal ObjectInstance Entries() => _engine.Realm.Intrinsics.SetIteratorPrototype.ConstructEntryIterator(this); diff --git a/Jint/Native/Object/ObjectInstance.cs b/Jint/Native/Object/ObjectInstance.cs index a3166da487..e0bbc2d424 100644 --- a/Jint/Native/Object/ObjectInstance.cs +++ b/Jint/Native/Object/ObjectInstance.cs @@ -2470,10 +2470,7 @@ bool TryGetValue(ulong idx, out JsValue jsValue) var callable = GetCallable(callbackfn); - // args is rented from the pool whose factory allocates new JsValue[3], so it is an exact - // JsValue[]; the per-element fills below bypass the covariant array store type check. - var args = _engine._jsValueArrayPool.RentArray(3); - Arguments.WriteNoTypeCheck(args, 2, this); + var invoker = CallbackInvoker.Rent(_engine, callable, 3, this); // try/finally so the rented pool array is returned on every exit path: the early // return-on-match below and a periodic Check() throw both previously leaked it. @@ -2490,9 +2487,7 @@ bool TryGetValue(ulong idx, out JsValue jsValue) if (TryGetValue(k, out var kvalue) || visitUnassigned) { - Arguments.WriteNoTypeCheck(args, 0, kvalue); - Arguments.WriteNoTypeCheck(args, 1, k); - var testResult = callable.Call(thisArg, args); + var testResult = invoker.Call(thisArg, kvalue, k); if (TypeConverter.ToBoolean(testResult)) { index = k; @@ -2514,9 +2509,7 @@ bool TryGetValue(ulong idx, out JsValue jsValue) if (TryGetValue((ulong) k, out var kvalue) || visitUnassigned) { kvalue ??= Undefined; - Arguments.WriteNoTypeCheck(args, 0, kvalue); - Arguments.WriteNoTypeCheck(args, 1, k); - var testResult = callable.Call(thisArg, args); + var testResult = invoker.Call(thisArg, kvalue, k); if (TypeConverter.ToBoolean(testResult)) { index = (ulong) k; @@ -2529,7 +2522,7 @@ bool TryGetValue(ulong idx, out JsValue jsValue) } finally { - _engine._jsValueArrayPool.ReturnArray(args); + invoker.Return(); } index = 0; diff --git a/Jint/Native/TypedArray/IntrinsicTypedArrayConstructor.cs b/Jint/Native/TypedArray/IntrinsicTypedArrayConstructor.cs index 685c07d0ad..a113a8ec85 100644 --- a/Jint/Native/TypedArray/IntrinsicTypedArrayConstructor.cs +++ b/Jint/Native/TypedArray/IntrinsicTypedArrayConstructor.cs @@ -85,7 +85,7 @@ private JsValue From(JsValue thisObject, JsValue source, JsValue mapFunction, Js var targetObj = TypedArrayCreate(_realm, (IConstructor) c, argumentList); targetObj._viewedArrayBuffer.AssertNotImmutable(); - var mappingArgs = mapping ? new JsValue[2] : null; + var invoker = mapping ? CallbackInvoker.Create(_engine, (ICallable) mapFunction, 2) : default; for (uint k = 0; k < len; ++k) { var Pk = JsNumber.Create(k); @@ -93,9 +93,7 @@ private JsValue From(JsValue thisObject, JsValue source, JsValue mapFunction, Js JsValue mappedValue; if (mapping) { - mappingArgs![0] = kValue; - mappingArgs[1] = Pk; - mappedValue = ((ICallable) mapFunction).Call(thisArg, mappingArgs); + mappedValue = invoker.Call(thisArg, kValue, Pk); } else { diff --git a/Jint/Native/TypedArray/IntrinsicTypedArrayPrototype.cs b/Jint/Native/TypedArray/IntrinsicTypedArrayPrototype.cs index 1788d4ba52..2de6413a39 100644 --- a/Jint/Native/TypedArray/IntrinsicTypedArrayPrototype.cs +++ b/Jint/Native/TypedArray/IntrinsicTypedArrayPrototype.cs @@ -361,10 +361,7 @@ private JsValue Every(JsValue thisObject, JsValue callbackFn, JsValue thisArg) var predicate = GetCallable(callbackFn); - // args is rented from the pool whose factory allocates new JsValue[3], so it is an exact - // JsValue[]; the per-element fills below bypass the covariant array store type check. - var args = _engine._jsValueArrayPool.RentArray(3); - Arguments.WriteNoTypeCheck(args, 2, o); + var invoker = CallbackInvoker.Rent(_engine, predicate, 3, o); for (var k = 0; k < len; k++) { if (k > 0 && k % ConstraintCheckInterval == 0) @@ -372,15 +369,13 @@ private JsValue Every(JsValue thisObject, JsValue callbackFn, JsValue thisArg) _engine.Constraints.Check(); } - Arguments.WriteNoTypeCheck(args, 0, o[k]); - Arguments.WriteNoTypeCheck(args, 1, k); - if (!TypeConverter.ToBoolean(predicate.Call(thisArg, args))) + if (!TypeConverter.ToBoolean(invoker.Call(thisArg, o[k], k))) { return JsBoolean.False; } } - _engine._jsValueArrayPool.ReturnArray(args); + invoker.Return(); return JsBoolean.True; } @@ -489,10 +484,7 @@ private JsValue Filter(JsValue thisObject, JsValue callbackFn, JsValue thisArg) var kept = new List(); var captured = 0; - // args is rented from the pool whose factory allocates new JsValue[3], so it is an exact - // JsValue[]; the per-element fills below bypass the covariant array store type check. - var args = _engine._jsValueArrayPool.RentArray(3); - Arguments.WriteNoTypeCheck(args, 2, o); + var invoker = CallbackInvoker.Rent(_engine, callbackfn, 3, o); for (var k = 0; k < len; k++) { if (k > 0 && k % ConstraintCheckInterval == 0) @@ -501,9 +493,7 @@ private JsValue Filter(JsValue thisObject, JsValue callbackFn, JsValue thisArg) } var kValue = o[k]; - Arguments.WriteNoTypeCheck(args, 0, kValue); - Arguments.WriteNoTypeCheck(args, 1, k); - var selected = callbackfn.Call(thisArg, args); + var selected = invoker.Call(thisArg, kValue, k); if (TypeConverter.ToBoolean(selected)) { kept.Add(kValue); @@ -511,7 +501,7 @@ private JsValue Filter(JsValue thisObject, JsValue callbackFn, JsValue thisArg) } } - _engine._jsValueArrayPool.ReturnArray(args); + invoker.Return(); var a = _realm.Intrinsics.TypedArray.TypedArraySpeciesCreate(o, [captured], isWrite: true); for (var n = 0; n < captured; ++n) @@ -565,10 +555,7 @@ private KeyValuePair DoFind(JsValue thisObject, JsValue predic return new KeyValuePair(JsNumber.IntegerNegativeOne, Undefined); } - // args is rented from the pool whose factory allocates new JsValue[3], so it is an exact - // JsValue[]; the per-element fills below bypass the covariant array store type check. - var args = _engine._jsValueArrayPool.RentArray(3); - Arguments.WriteNoTypeCheck(args, 2, o); + var invoker = CallbackInvoker.Rent(_engine, predicate, 3, o); if (!fromEnd) { for (var k = 0; k < len; k++) @@ -580,9 +567,7 @@ private KeyValuePair DoFind(JsValue thisObject, JsValue predic var kNumber = JsNumber.Create(k); var kValue = o[k]; - Arguments.WriteNoTypeCheck(args, 0, kValue); - Arguments.WriteNoTypeCheck(args, 1, kNumber); - if (TypeConverter.ToBoolean(predicate.Call(thisArg, args))) + if (TypeConverter.ToBoolean(invoker.Call(thisArg, kValue, kNumber))) { return new KeyValuePair(kNumber, kValue); } @@ -599,9 +584,7 @@ private KeyValuePair DoFind(JsValue thisObject, JsValue predic var kNumber = JsNumber.Create(k); var kValue = o[k]; - Arguments.WriteNoTypeCheck(args, 0, kValue); - Arguments.WriteNoTypeCheck(args, 1, kNumber); - if (TypeConverter.ToBoolean(predicate.Call(thisArg, args))) + if (TypeConverter.ToBoolean(invoker.Call(thisArg, kValue, kNumber))) { return new KeyValuePair(kNumber, kValue); } @@ -623,10 +606,7 @@ private JsValue ForEach(JsValue thisObject, JsValue callbackFn, JsValue thisArg) var o = taRecord.Object; var len = taRecord.TypedArrayLength; - // args is rented from the pool whose factory allocates new JsValue[3], so it is an exact - // JsValue[]; the per-element fills below bypass the covariant array store type check. - var args = _engine._jsValueArrayPool.RentArray(3); - Arguments.WriteNoTypeCheck(args, 2, o); + var invoker = CallbackInvoker.Rent(_engine, callbackfn, 3, o); for (var k = 0; k < len; k++) { if (k > 0 && k % ConstraintCheckInterval == 0) @@ -634,13 +614,10 @@ private JsValue ForEach(JsValue thisObject, JsValue callbackFn, JsValue thisArg) _engine.Constraints.Check(); } - var kValue = o[k]; - Arguments.WriteNoTypeCheck(args, 0, kValue); - Arguments.WriteNoTypeCheck(args, 1, k); - callbackfn.Call(thisArg, args); + invoker.Call(thisArg, o[k], k); } - _engine._jsValueArrayPool.ReturnArray(args); + invoker.Return(); return Undefined; } @@ -908,10 +885,7 @@ private ObjectInstance Map(JsValue thisObject, JsValue callbackFn, JsValue thisA var callable = GetCallable(callbackFn); var a = _realm.Intrinsics.TypedArray.TypedArraySpeciesCreate(o, [len], isWrite: true); - // args is rented from the pool whose factory allocates new JsValue[3], so it is an exact - // JsValue[]; the per-element fills below bypass the covariant array store type check. - var args = _engine._jsValueArrayPool.RentArray(3); - Arguments.WriteNoTypeCheck(args, 2, o); + var invoker = CallbackInvoker.Rent(_engine, callable, 3, o); for (var k = 0; k < len; k++) { if (k > 0 && k % ConstraintCheckInterval == 0) @@ -919,13 +893,11 @@ private ObjectInstance Map(JsValue thisObject, JsValue callbackFn, JsValue thisA _engine.Constraints.Check(); } - Arguments.WriteNoTypeCheck(args, 0, o[k]); - Arguments.WriteNoTypeCheck(args, 1, k); - var mappedValue = callable.Call(thisArg, args); + var mappedValue = invoker.Call(thisArg, o[k], k); a[k] = mappedValue; } - _engine._jsValueArrayPool.ReturnArray(args); + invoker.Return(); return a; } @@ -961,10 +933,7 @@ private JsValue Reduce(JsValue thisObject, JsCallArguments arguments) k++; } - // args is rented from the pool whose factory allocates new JsValue[4], so it is an exact - // JsValue[]; the per-element fills below bypass the covariant array store type check. - var args = _engine._jsValueArrayPool.RentArray(4); - Arguments.WriteNoTypeCheck(args, 3, o); + var invoker = CallbackInvoker.Rent(_engine, callbackfn, 4, o); while (k < len) { if (k > 0 && k % ConstraintCheckInterval == 0) @@ -973,14 +942,11 @@ private JsValue Reduce(JsValue thisObject, JsCallArguments arguments) } var kValue = o[k]; - Arguments.WriteNoTypeCheck(args, 0, accumulator); - Arguments.WriteNoTypeCheck(args, 1, kValue); - Arguments.WriteNoTypeCheck(args, 2, k); - accumulator = callbackfn.Call(Undefined, args); + accumulator = invoker.Call(Undefined, accumulator, kValue, k); k++; } - _engine._jsValueArrayPool.ReturnArray(args); + invoker.Return(); return accumulator; } @@ -1016,10 +982,7 @@ private JsValue ReduceRight(JsValue thisObject, JsCallArguments arguments) k--; } - // jsValues is rented from the pool whose factory allocates new JsValue[4], so it is an exact - // JsValue[]; the per-element fills below bypass the covariant array store type check. - var jsValues = _engine._jsValueArrayPool.RentArray(4); - Arguments.WriteNoTypeCheck(jsValues, 3, o); + var invoker = CallbackInvoker.Rent(_engine, callbackfn, 4, o); for (; k >= 0; k--) { if (k % ConstraintCheckInterval == 0) @@ -1027,13 +990,10 @@ private JsValue ReduceRight(JsValue thisObject, JsCallArguments arguments) _engine.Constraints.Check(); } - Arguments.WriteNoTypeCheck(jsValues, 0, accumulator); - Arguments.WriteNoTypeCheck(jsValues, 1, o[(int) k]); - Arguments.WriteNoTypeCheck(jsValues, 2, k); - accumulator = callbackfn.Call(Undefined, jsValues); + accumulator = invoker.Call(Undefined, accumulator, o[(int) k], k); } - _engine._jsValueArrayPool.ReturnArray(jsValues); + invoker.Return(); return accumulator; } @@ -1405,10 +1365,7 @@ private JsValue Some(JsValue thisObject, JsValue callbackFn, JsValue thisArg) var callbackfn = GetCallable(callbackFn); - // args is rented from the pool whose factory allocates new JsValue[3], so it is an exact - // JsValue[]; the per-element fills below bypass the covariant array store type check. - var args = _engine._jsValueArrayPool.RentArray(3); - Arguments.WriteNoTypeCheck(args, 2, o); + var invoker = CallbackInvoker.Rent(_engine, callbackfn, 3, o); for (var k = 0; k < len; k++) { if (k > 0 && k % ConstraintCheckInterval == 0) @@ -1416,15 +1373,13 @@ private JsValue Some(JsValue thisObject, JsValue callbackFn, JsValue thisArg) _engine.Constraints.Check(); } - Arguments.WriteNoTypeCheck(args, 0, o[k]); - Arguments.WriteNoTypeCheck(args, 1, k); - if (TypeConverter.ToBoolean(callbackfn.Call(thisArg, args))) + if (TypeConverter.ToBoolean(invoker.Call(thisArg, o[k], k))) { return JsBoolean.True; } } - _engine._jsValueArrayPool.ReturnArray(args); + invoker.Return(); return JsBoolean.False; } @@ -1872,12 +1827,18 @@ public static TypedArrayComparer WithFunction(JsArrayBuffer buffer, ICallable? c private readonly JsArrayBuffer _buffer; private readonly ICallable? _compare; - private readonly JsValue[] _comparableArray = new JsValue[2]; + // Built once per sort rather than per comparison: the comparator cannot change between two + // comparisons, so neither can the lane it is dispatched through. + private readonly CallbackInvoker _invoker; private TypedArrayComparer(JsArrayBuffer buffer, ICallable? compare) { _buffer = buffer; _compare = compare; + if (compare is not null) + { + _invoker = CallbackInvoker.Create(buffer.Engine, compare, 2); + } } public int Compare(JsValue? x, JsValue? y) @@ -1904,12 +1865,7 @@ public int Compare(JsValue? x, JsValue? y) if (_compare is not null) { - // _comparableArray is an exact JsValue[2]; bypass the per-comparison covariance check - // (stelem.ref -> CastHelpers.StelemRef) a plain store pays because JsValue is not sealed. - Arguments.WriteNoTypeCheck(_comparableArray, 0, x); - Arguments.WriteNoTypeCheck(_comparableArray, 1, y); - - var v = TypeConverter.ToNumber(_compare.Call(Undefined, _comparableArray)); + var v = TypeConverter.ToNumber(_invoker.Call(Undefined, x, y)); if (double.IsNaN(v)) { From fdf75ed3eee372782577ec9be4f27b9ea9587c4b Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Fri, 31 Jul 2026 17:41:04 +0300 Subject: [PATCH 2/2] Keep the sort comparers on a plain argument array ArrayComparer and TypedArrayComparer are dereferenced once per comparison of an n log n sort, and holding a CallbackInvoker by value grew each comparer object by the invoker's eight fields. Measured against merged main, both A/B orderings: SortWithComparer_1K +4.27% (own variance 2.3%) and SortReverseSorted_1K +2.10% (1.3%) -- the latter uses the DEFAULT comparison and never calls the invoker at all, which is what identifies the object size rather than the dispatch as the cost. The per-element callbacks keep the invoker and keep their wins (Filter/10000 -12.9%, MapFilterReduceChain -11.7%, ForEachSum -8.6%). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01G24ppn8iSgjo1APp3YkzTK --- Jint/Native/Array/ArrayPrototype.cs | 21 +++++++++++-------- .../IntrinsicTypedArrayPrototype.cs | 18 ++++++++-------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/Jint/Native/Array/ArrayPrototype.cs b/Jint/Native/Array/ArrayPrototype.cs index 0ceb789df8..0b6086adb9 100644 --- a/Jint/Native/Array/ArrayPrototype.cs +++ b/Jint/Native/Array/ArrayPrototype.cs @@ -1,4 +1,4 @@ -#pragma warning disable CA1859 // Use concrete types when possible for improved performance -- most of prototype methods return JsValue +#pragma warning disable CA1859 // Use concrete types when possible for improved performance -- most of prototype methods return JsValue using System.Linq; using System.Text; @@ -2575,18 +2575,16 @@ public static ArrayComparer WithFunction(Engine engine, ICallable? compare) private readonly Engine? _engine; private readonly ICallable? _compare; - // Built once per sort rather than per comparison: the comparator cannot change between two - // comparisons, so neither can the lane it is dispatched through. - private readonly CallbackInvoker _invoker; + // Deliberately a plain JsValue[2] and not a CallbackInvoker: this comparer is dereferenced + // on every comparison of an n log n sort, and embedding the invoker struct by value grew the + // object enough to cost ~4% on SortWithComparer_1K and ~2% on the default-comparison rows, + // which never touch it at all. Measured, both A/B orderings. + private readonly JsValue[] _comparableArray = new JsValue[2]; private ArrayComparer(Engine? engine, ICallable? compare) { _engine = engine; _compare = compare; - if (compare is not null) - { - _invoker = CallbackInvoker.Create(engine!, compare, 2); - } } public int Compare(JsValue? x, JsValue? y) @@ -2632,7 +2630,12 @@ public int Compare(JsValue? x, JsValue? y) { _engine!.RunBeforeExecuteStatementChecks(null); - var s = TypeConverter.ToNumber(_invoker.Call(Undefined, x!, y!)); + // _comparableArray is an exact JsValue[2]; bypass the per-comparison covariance check + // (stelem.ref -> CastHelpers.StelemRef) a plain store pays because JsValue is not sealed. + Arguments.WriteNoTypeCheck(_comparableArray, 0, x!); + Arguments.WriteNoTypeCheck(_comparableArray, 1, y!); + + var s = TypeConverter.ToNumber(_compare.Call(Undefined, _comparableArray)); if (s < 0) { return -1; diff --git a/Jint/Native/TypedArray/IntrinsicTypedArrayPrototype.cs b/Jint/Native/TypedArray/IntrinsicTypedArrayPrototype.cs index 2de6413a39..3e91b2f230 100644 --- a/Jint/Native/TypedArray/IntrinsicTypedArrayPrototype.cs +++ b/Jint/Native/TypedArray/IntrinsicTypedArrayPrototype.cs @@ -1,4 +1,4 @@ -#pragma warning disable CA1859 // Use concrete types when possible for improved performance -- most of prototype methods return JsValue +#pragma warning disable CA1859 // Use concrete types when possible for improved performance -- most of prototype methods return JsValue using System.Linq; using System.Runtime.InteropServices; @@ -1827,18 +1827,15 @@ public static TypedArrayComparer WithFunction(JsArrayBuffer buffer, ICallable? c private readonly JsArrayBuffer _buffer; private readonly ICallable? _compare; - // Built once per sort rather than per comparison: the comparator cannot change between two - // comparisons, so neither can the lane it is dispatched through. - private readonly CallbackInvoker _invoker; + // Plain JsValue[2], not a CallbackInvoker — see the note on ArrayPrototype.ArrayComparer: + // a sort dereferences its comparer once per comparison, and embedding the invoker by value + // measurably regressed the sort rows. + private readonly JsValue[] _comparableArray = new JsValue[2]; private TypedArrayComparer(JsArrayBuffer buffer, ICallable? compare) { _buffer = buffer; _compare = compare; - if (compare is not null) - { - _invoker = CallbackInvoker.Create(buffer.Engine, compare, 2); - } } public int Compare(JsValue? x, JsValue? y) @@ -1865,7 +1862,10 @@ public int Compare(JsValue? x, JsValue? y) if (_compare is not null) { - var v = TypeConverter.ToNumber(_invoker.Call(Undefined, x, y)); + Arguments.WriteNoTypeCheck(_comparableArray, 0, x); + Arguments.WriteNoTypeCheck(_comparableArray, 1, y); + + var v = TypeConverter.ToNumber(_compare.Call(Undefined, _comparableArray)); if (double.IsNaN(v)) {