From d9c78822a66ce746999c89672b017cf071e7ea6d Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Mon, 13 Jul 2026 15:48:16 +0300 Subject: [PATCH] Replace flag-proven casts with Unsafe.As on hot arithmetic paths ETW profiling showed CastHelpers.ChkCastClass at ~0.7% on hot interpreter paths. The JIT cannot correlate an InternalTypes flag test with the runtime type, so a plain (T) cast after such a check still emits a castclass. Apply the established Unsafe.As-after- flag-check idiom (see ObjectInstance shape-mode paths) to the enumerated hot arithmetic sites: - JintBinaryExpression: (JsNumber) operand casts in the Plus/Minus/Times/Divide double lanes, each directly dominated by left/right._type == InternalTypes.Number checks; (JsBoolean) casts on Compare(...) results in the four relational classes, guarded by the not-undefined check (Compare's return set is closed: JsBoolean.True/False or undefined on every path). - JintExpression.CompareNumber: (JsNumber) casts; sole caller Compare has proven both operands via IsNumber(), documented with a Debug.Assert precondition. - JintUnaryExpression.EvaluateMinus: (JsNumber) cast after the post-ToNumeric IsNumber() check. - JsValueExtensions.AsNumber: cast after the method's own IsNumber() guard (throw helper is [DoesNotReturn]), with a Debug.Assert beside it. AsInteger is intentionally left unchanged: it has no in-method guard and IConvertible.ToInt32 (JsValue.Convertible.cs) invokes it unguarded on arbitrary JsValues, so Unsafe.As there would turn a user-visible InvalidCastException into type confusion. The Exponentiate (JsNumber) casts are likewise skipped: their guard (AreNonBigIntOperands) only proves not-BigInt; JsNumber-ness rests on callers' ToNumeric contract rather than a local flag check. Co-Authored-By: Claude Fable 5 --- Jint/JsValueExtensions.cs | 4 +++- .../Expressions/JintBinaryExpression.cs | 20 +++++++++++-------- .../Interpreter/Expressions/JintExpression.cs | 12 +++++++---- .../Expressions/JintUnaryExpression.cs | 3 ++- 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/Jint/JsValueExtensions.cs b/Jint/JsValueExtensions.cs index 7e90fe454..3563c0c18 100644 --- a/Jint/JsValueExtensions.cs +++ b/Jint/JsValueExtensions.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Diagnostics.Contracts; using System.Numerics; using System.Runtime.CompilerServices; @@ -236,7 +237,8 @@ public static double AsNumber(this JsValue value) ThrowWrongTypeException(value, "number"); } - return ((JsNumber) value)._value; + Debug.Assert(value is JsNumber); + return Unsafe.As(value)._value; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Jint/Runtime/Interpreter/Expressions/JintBinaryExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintBinaryExpression.cs index 7a8490982..3622f892e 100644 --- a/Jint/Runtime/Interpreter/Expressions/JintBinaryExpression.cs +++ b/Jint/Runtime/Interpreter/Expressions/JintBinaryExpression.cs @@ -1271,7 +1271,8 @@ public override bool GetBooleanValue(EvaluationContext context) } var value = Compare(left, right); - return value._type != InternalTypes.Undefined && ((JsBoolean) value)._value; + // Compare only ever returns JsBoolean.True/False or undefined + return value._type != InternalTypes.Undefined && Unsafe.As(value)._value; } } @@ -1326,7 +1327,8 @@ public override bool GetBooleanValue(EvaluationContext context) } var value = Compare(right, left, false); - return value._type != InternalTypes.Undefined && ((JsBoolean) value)._value; + // Compare only ever returns JsBoolean.True/False or undefined + return value._type != InternalTypes.Undefined && Unsafe.As(value)._value; } } @@ -1364,7 +1366,7 @@ protected override object EvaluateInternal(EvaluationContext context) if (left._type == InternalTypes.Number && right._type == InternalTypes.Number) { - return JsNumber.Create(((JsNumber) left)._value + ((JsNumber) right)._value); + return JsNumber.Create(Unsafe.As(left)._value + Unsafe.As(right)._value); } var lprim = TypeConverter.ToPrimitive(left); @@ -1506,7 +1508,7 @@ protected override object EvaluateInternal(EvaluationContext context) if (left._type == InternalTypes.Number && right._type == InternalTypes.Number) { - return JsNumber.Create(((JsNumber) left)._value - ((JsNumber) right)._value); + return JsNumber.Create(Unsafe.As(left)._value - Unsafe.As(right)._value); } left = TypeConverter.ToNumeric(left); @@ -1567,7 +1569,7 @@ protected override object EvaluateInternal(EvaluationContext context) } else if (left._type == InternalTypes.Number && right._type == InternalTypes.Number) { - result = JsNumber.Create(((JsNumber) left)._value * ((JsNumber) right)._value); + result = JsNumber.Create(Unsafe.As(left)._value * Unsafe.As(right)._value); } else { @@ -1618,7 +1620,7 @@ protected override object EvaluateInternal(EvaluationContext context) if (left._type == InternalTypes.Number && right._type == InternalTypes.Number) { - return JsNumber.Create(((JsNumber) left)._value / ((JsNumber) right)._value); + return JsNumber.Create(Unsafe.As(left)._value / Unsafe.As(right)._value); } left = TypeConverter.ToNumeric(left); @@ -1739,7 +1741,8 @@ protected override object EvaluateInternal(EvaluationContext context) var right = _leftFirst ? rightValue : leftValue; var value = Compare(left, right, _leftFirst); - return value.IsUndefined() || ((JsBoolean) value)._value ? JsBoolean.False : JsBoolean.True; + // Compare only ever returns JsBoolean.True/False or undefined + return value.IsUndefined() || Unsafe.As(value)._value ? JsBoolean.False : JsBoolean.True; } public override bool GetBooleanValue(EvaluationContext context) @@ -1764,7 +1767,8 @@ public override bool GetBooleanValue(EvaluationContext context) var right = _leftFirst ? rightValue : leftValue; var value = Compare(left, right, _leftFirst); - return !value.IsUndefined() && !((JsBoolean) value)._value; + // Compare only ever returns JsBoolean.True/False or undefined + return !value.IsUndefined() && !Unsafe.As(value)._value; } } diff --git a/Jint/Runtime/Interpreter/Expressions/JintExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintExpression.cs index 86ec549d8..fd98d4b68 100644 --- a/Jint/Runtime/Interpreter/Expressions/JintExpression.cs +++ b/Jint/Runtime/Interpreter/Expressions/JintExpression.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Numerics; using System.Runtime.CompilerServices; using Jint.Native; @@ -392,6 +393,9 @@ protected static JsValue Compare(JsValue x, JsValue y, bool leftFirst = true) => private static JsValue CompareNumber(JsValue x, JsValue y, bool leftFirst) { + // only called from Compare which has proven both operands via IsNumber() + Debug.Assert(x is JsNumber && y is JsNumber); + if (x.IsInteger() && y.IsInteger()) { return x.AsInteger() < y.AsInteger() ? JsBoolean.True : JsBoolean.False; @@ -400,13 +404,13 @@ private static JsValue CompareNumber(JsValue x, JsValue y, bool leftFirst) double nx, ny; if (leftFirst) { - nx = ((JsNumber) x)._value; - ny = ((JsNumber) y)._value; + nx = Unsafe.As(x)._value; + ny = Unsafe.As(y)._value; } else { - ny = ((JsNumber) y)._value; - nx = ((JsNumber) x)._value; + ny = Unsafe.As(y)._value; + nx = Unsafe.As(x)._value; } if (double.IsNaN(nx) || double.IsNaN(ny)) diff --git a/Jint/Runtime/Interpreter/Expressions/JintUnaryExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintUnaryExpression.cs index 14e13ef19..b4de3368d 100644 --- a/Jint/Runtime/Interpreter/Expressions/JintUnaryExpression.cs +++ b/Jint/Runtime/Interpreter/Expressions/JintUnaryExpression.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.Numerics; using System.Reflection; +using System.Runtime.CompilerServices; using Environment = Jint.Runtime.Environments.Environment; @@ -297,7 +298,7 @@ internal static JsValue EvaluateMinus(JsValue value) value = TypeConverter.ToNumeric(value); if (value.IsNumber()) { - var n = ((JsNumber) value)._value; + var n = Unsafe.As(value)._value; return double.IsNaN(n) ? JsNumber.DoubleNaN : JsNumber.Create(n * -1); }