From aaac911b1003d41c5fa477a1a12cf5d0470f5e31 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Mon, 22 Jun 2026 00:23:28 -0700 Subject: [PATCH] =?UTF-8?q?Perf=20#878:=20Float64Array=20element=20access?= =?UTF-8?q?=20fast=20path=20=E2=80=94=20unboxed=20get/set?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compiled Float64Array element access (`a[i]` read, `a[i] = x` write) routed through the boxed dispatcher chain Runtime.GetIndex → GetTypedArrayElement (isinst + castclass + virtual Get) → BitConverter + Box per element. With no typed fast path (unlike number[] → List per #857), Float64Array ran 24–101× slower than Node — ironically slower than a plain number[]. Add unboxed `double GetUnboxed(int)` / `void SetUnboxed(int, double)` on the emitted $Float64Array (mirroring the boxed byte logic without the box or Convert.ToDouble coercion), and bind them directly in EmitGetIndex/EmitSetIndex when the receiver is a variable statically typed Float64Array (write also gated on a statically-numeric RHS; a non-numeric RHS falls back to the boxed path for ToNumber coercion). Reads leave a native double on the stack; writes take one — no GetIndex/SetIndex dispatch, no isinst, no per-element box. Calls stay in the output assembly → fully standalone. OOB faults exactly as the boxed Get/Set do today (both SharpTS modes already throw on OOB — semantics unchanged). Result (Float64Array fill + 3-point stencil, n=100000): ~33x → ~3.4x vs Node. Verification: - IL verifies (--verify); 223 ILVerification/typed-array + 51 more unit tests. - Compiled output BYTE-IDENTICAL to the prior boxed path across fill/stencil, NaN/±Inf/-0, assignment-result, non-numeric-RHS coercion fallback, and OOB. - Test262 (633 TypedArray index-semantics files, in-process compiled): per-file outcomes identical base-vs-fix — zero conformance change. Residual ~3.4x is the byte[]+BitConverter backing and the per-element accessor call vs V8's raw double memory — a follow-up (sealed concrete type for JIT devirtualization/inlining, or a native double[] backing). --- Compilation/EmittedRuntime.cs | 4 ++ Compilation/ILEmitter.Properties.cs | 52 ++++++++++++++++++ Compilation/RuntimeEmitter.TSTypedArray.cs | 64 ++++++++++++++++++++++ 3 files changed, 120 insertions(+) diff --git a/Compilation/EmittedRuntime.cs b/Compilation/EmittedRuntime.cs index 9276bb30..8d5b81ce 100644 --- a/Compilation/EmittedRuntime.cs +++ b/Compilation/EmittedRuntime.cs @@ -1586,6 +1586,10 @@ public class EmittedRuntime public MethodBuilder TypedArrayGetBuffer { get; set; } = null!; public MethodBuilder TypedArrayElementGet { get; set; } = null!; public MethodBuilder TypedArrayElementSet { get; set; } = null!; + // Unboxed Float64Array element accessors (#878) — direct double get/set on the + // concrete $Float64Array, bypassing the boxed Get/Set + GetIndex dispatch. + public MethodBuilder Float64ArrayGetUnboxed { get; set; } = null!; + public MethodBuilder Float64ArraySetUnboxed { get; set; } = null!; // Concrete TypedArray types (pure-IL for standalone DLLs) public TypeBuilder Int8ArrayType { get; set; } = null!; diff --git a/Compilation/ILEmitter.Properties.cs b/Compilation/ILEmitter.Properties.cs index 54920777..31a7ec9d 100644 --- a/Compilation/ILEmitter.Properties.cs +++ b/Compilation/ILEmitter.Properties.cs @@ -858,6 +858,25 @@ protected override void EmitGetIndex(Expr.GetIndex gi) return; } + // Float64Array fast path (#878): when the receiver is a variable statically typed + // Float64Array, read the element UNBOXED via $Float64Array.GetUnboxed → native double + // on the stack. Eliminates the Runtime.GetIndex dispatch, the GetTypedArrayElement + // isinst/castclass, the virtual Get, and the per-element box. Out-of-range access + // faults via BitConverter exactly as the boxed path does today (OOB semantics + // unchanged). Receiver is a side-effect-free variable, so it is loaded once. + if (!gi.Optional && gi.Object is Expr.Variable + && _ctx.TypeMap?.Get(gi.Object) is TypeInfo.TypedArray { ElementType: "Float64" }) + { + EmitExpression(gi.Object); + EnsureBoxed(); + IL.Emit(OpCodes.Castclass, _ctx.Runtime!.Float64ArrayType); + EmitExpressionAsDouble(gi.Index); + IL.Emit(OpCodes.Conv_I4); + IL.Emit(OpCodes.Callvirt, _ctx.Runtime!.Float64ArrayGetUnboxed); + SetStackType(StackType.Double); + return; + } + // Descriptor-driven fast path: when receiver is statically known to be an array, // emit direct List access — skips runtime type dispatch, // index boxing, and Convert.ToInt32(object) overhead. @@ -1041,6 +1060,39 @@ protected override void EmitSetIndex(Expr.SetIndex si) return; } + // Float64Array fast path (#878): variable statically typed Float64Array with a + // statically-numeric RHS — write the element UNBOXED via $Float64Array.SetUnboxed. + // Eliminates the Runtime.SetIndex dispatch, the isinst, the value box, and the + // Convert.ToDouble coercion. A non-numeric RHS falls through to the boxed path, + // which performs JS ToNumber coercion. OOB faults exactly as the boxed path does. + // Evaluate index then value (the receiver var is side-effect-free). + if (si.Object is Expr.Variable + && _ctx.TypeMap?.Get(si.Object) is TypeInfo.TypedArray { ElementType: "Float64" } + && _ctx.TypeMap?.Get(si.Value) is TypeInfo.Primitive { Type: TokenType.TYPE_NUMBER }) + { + EmitExpressionAsDouble(si.Index); + IL.Emit(OpCodes.Conv_I4); + var idxLocal = IL.DeclareLocal(_ctx.Types.Int32); + IL.Emit(OpCodes.Stloc, idxLocal); + + EmitExpression(si.Value); + EnsureDouble(); + var valLocal = IL.DeclareLocal(_ctx.Types.Double); + IL.Emit(OpCodes.Stloc, valLocal); + + EmitExpression(si.Object); + EnsureBoxed(); + IL.Emit(OpCodes.Castclass, _ctx.Runtime!.Float64ArrayType); + IL.Emit(OpCodes.Ldloc, idxLocal); + IL.Emit(OpCodes.Ldloc, valLocal); + IL.Emit(OpCodes.Callvirt, _ctx.Runtime!.Float64ArraySetUnboxed); + + // Assignment expression result: the (unboxed) assigned value. + IL.Emit(OpCodes.Ldloc, valLocal); + SetStackType(StackType.Double); + return; + } + // Descriptor-driven fast path: when receiver is statically known to be an array, // emit direct List access with auto-extension — skips runtime type dispatch, // index boxing, and Convert.ToInt32(object) overhead. diff --git a/Compilation/RuntimeEmitter.TSTypedArray.cs b/Compilation/RuntimeEmitter.TSTypedArray.cs index 57b9bacc..cb19b51d 100644 --- a/Compilation/RuntimeEmitter.TSTypedArray.cs +++ b/Compilation/RuntimeEmitter.TSTypedArray.cs @@ -737,5 +737,69 @@ private void EmitTypedArrayIndexer( setIl.Emit(OpCodes.Ret); typeBuilder.DefineMethodOverride(setter, runtime.TypedArrayElementSet); + + // Unboxed double accessors for Float64Array (#878). These mirror the byte + // logic of the boxed Get/Set above but take/return a native `double` — no + // Box, no Convert.ToDouble coercion. The IL emitter binds them directly at + // statically-known Float64Array index sites, eliminating the GetIndex/SetIndex + // dispatch, the isinst ladder, and the per-element box. Like Get/Set, they do + // NOT bounds-check: out-of-range access faults via BitConverter/Array.Copy, + // exactly as the boxed path does today (OOB semantics unchanged). + if (bytesPerElement == 8 && isFloat) + { + EmitFloat64UnboxedAccessors(typeBuilder, runtime); + } + } + + // double GetUnboxed(int index) / void SetUnboxed(int index, double value) on $Float64Array. + private void EmitFloat64UnboxedAccessors(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var getU = typeBuilder.DefineMethod( + "GetUnboxed", + MethodAttributes.Public | MethodAttributes.HideBySig, + _types.Double, + [_types.Int32] + ); + var gil = getU.GetILGenerator(); + // return BitConverter.ToDouble(_buffer, _byteOffset + index * 8); + gil.Emit(OpCodes.Ldarg_0); + gil.Emit(OpCodes.Ldfld, _typedArrayBufferField!); + gil.Emit(OpCodes.Ldarg_0); + gil.Emit(OpCodes.Ldfld, _typedArrayByteOffsetField!); + gil.Emit(OpCodes.Ldarg_1); + gil.Emit(OpCodes.Ldc_I4_8); + gil.Emit(OpCodes.Mul); + gil.Emit(OpCodes.Add); + gil.Emit(OpCodes.Call, typeof(BitConverter).GetMethod("ToDouble", [typeof(byte[]), typeof(int)])!); + gil.Emit(OpCodes.Ret); + runtime.Float64ArrayGetUnboxed = getU; + + var setU = typeBuilder.DefineMethod( + "SetUnboxed", + MethodAttributes.Public | MethodAttributes.HideBySig, + _types.Void, + [_types.Int32, _types.Double] + ); + var sil = setU.GetILGenerator(); + var bytesLocal = sil.DeclareLocal(typeof(byte[])); + // var bytes = BitConverter.GetBytes(value); + sil.Emit(OpCodes.Ldarg_2); + sil.Emit(OpCodes.Call, typeof(BitConverter).GetMethod("GetBytes", [typeof(double)])!); + sil.Emit(OpCodes.Stloc, bytesLocal); + // Array.Copy(bytes, 0, _buffer, _byteOffset + index * 8, 8); + sil.Emit(OpCodes.Ldloc, bytesLocal); + sil.Emit(OpCodes.Ldc_I4_0); + sil.Emit(OpCodes.Ldarg_0); + sil.Emit(OpCodes.Ldfld, _typedArrayBufferField!); + sil.Emit(OpCodes.Ldarg_0); + sil.Emit(OpCodes.Ldfld, _typedArrayByteOffsetField!); + sil.Emit(OpCodes.Ldarg_1); + sil.Emit(OpCodes.Ldc_I4_8); + sil.Emit(OpCodes.Mul); + sil.Emit(OpCodes.Add); + sil.Emit(OpCodes.Ldc_I4_8); + sil.Emit(OpCodes.Call, typeof(Array).GetMethod("Copy", [typeof(Array), typeof(int), typeof(Array), typeof(int), typeof(int)])!); + sil.Emit(OpCodes.Ret); + runtime.Float64ArraySetUnboxed = setU; } }