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; } }