From 5d0379c9ac10fd00b31cfa3bb9a08535c0a97c3a Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Tue, 22 Sep 2026 14:55:16 -0700 Subject: [PATCH 1/2] Preserve operand execution order during JIT morphing Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/coreclr/jit/morph.cpp | 157 +++++++++++---- .../JIT/Regression_ro_2/Runtime_134334.cs | 188 ++++++++++++++++++ 2 files changed, 310 insertions(+), 35 deletions(-) create mode 100644 src/tests/JIT/Regression_ro_2/Runtime_134334.cs diff --git a/src/coreclr/jit/morph.cpp b/src/coreclr/jit/morph.cpp index ed49402b9db54a..3a5fdac0fd20a7 100644 --- a/src/coreclr/jit/morph.cpp +++ b/src/coreclr/jit/morph.cpp @@ -170,15 +170,25 @@ class SharedTempsScope // tree - The node that is changed. This must be a large node. // helper - The helper. // morphArgs - Whether to call fgMorphArgs after adding the args. -// arg1, arg2 - Optional arguments to add to the call. +// arg1, arg2 - Optional arguments to add to the call, in physical operand order. // // Return value: -// The call (which is the same as `tree`). +// The call, optionally preceded by a comma evaluating the second operand. // GenTree* Compiler::fgMorphIntoHelperCall(GenTree* tree, int helper, bool morphArgs, GenTree* arg1, GenTree* arg2) { + GenTree* arg2Defn = nullptr; + if ((arg2 != nullptr) && tree->IsReverseOp() && !arg2->IsInvariant()) + { + // Helper arguments keep their physical positions, but arg2 must execute before arg1. + TempInfo temp = fgMakeTemp(arg2); + arg2Defn = temp.store; + arg2 = temp.load; + } + // The helper call ought to be semantically equivalent to the original node, so preserve its VN. tree->ChangeOper(GT_CALL, GenTree::PRESERVE_VN); + tree->ClearReverseOp(); GenTreeCall* call = tree->AsCall(); // Args are cleared by ChangeOper above @@ -245,11 +255,22 @@ GenTree* Compiler::fgMorphIntoHelperCall(GenTree* tree, int helper, bool morphAr if (morphArgs) { SharedTempsScope scope(this); + if (arg2Defn != nullptr) + { + arg2Defn = fgMorphTree(arg2Defn); + } tree = fgMorphArgs(call); } tree->SetMorphed(this); + if (arg2Defn != nullptr) + { + tree = gtNewOperNode(GT_COMMA, tree->TypeGet(), arg2Defn, tree); + tree->SetVNsFromNode(call); + tree->SetMorphed(this); + } + return tree; } @@ -2043,16 +2064,6 @@ GenTreeCall* Compiler::fgMorphArgs(GenTreeCall* call) call->gtArgs.AddFinalArgsAndDetermineABIInfo(this, call); JITDUMP("%sMorphing args for %d.%s:\n", (reMorphing) ? "Re" : "", call->gtTreeID, GenTree::OpName(call->gtOper)); - // If we are remorphing, process the late arguments (which were determined by a previous caller). - if (reMorphing) - { - for (CallArg& arg : call->gtArgs.LateArgs()) - { - arg.SetLateNode(fgMorphTree(arg.GetLateNode())); - flagsSummary |= arg.GetLateNode()->gtFlags; - } - } - // First we morph the argument subtrees ('this' pointer, arguments, etc.). // During the first call to fgMorphArgs we also record the // information about late arguments in CallArgs. @@ -2123,7 +2134,16 @@ GenTreeCall* Compiler::fgMorphArgs(GenTreeCall* call) } // end foreach argument loop - if (!reMorphing) + // Late arguments execute after the early arguments, including on remorph. + if (reMorphing) + { + for (CallArg& arg : call->gtArgs.LateArgs()) + { + arg.SetLateNode(fgMorphTree(arg.GetLateNode())); + flagsSummary |= arg.GetLateNode()->gtFlags; + } + } + else { call->gtArgs.ArgsComplete(this, call); } @@ -2926,6 +2946,19 @@ GenTree* Compiler::fgMorphIndexAddr(GenTreeIndexAddr* indexAddr) noway_assert(!varTypeIsStruct(elemTyp) || (elemStructType != NO_CLASS_HANDLE)); + if (indexAddr->IsReverseOp()) + { + indexAddr->ClearReverseOp(); + GenTree* index = indexAddr->Index(); + if (!index->IsInvariant()) + { + // Capture the index before the array so both the retained and expanded forms use normal operand order. + TempInfo temp = fgMakeTemp(index); + indexAddr->Index() = temp.load; + return fgMorphTree(gtNewOperNode(GT_COMMA, indexAddr->TypeGet(), temp.store, indexAddr)); + } + } + // In minopts, we will not be expanding GT_INDEX_ADDR in order to minimize the size of the IR. As minopts // compilation time is roughly proportional to the size of the IR, this helps keep compilation times down. // Furthermore, this representation typically saves on code size in minopts w.r.t. the complete expansion @@ -6975,7 +7008,7 @@ GenTree* Compiler::fgMorphSmpOp(GenTree* tree, bool* optAssertionPropDone) /* The steps in this function are : o Perform required preorder processing - o Process the first, then second operand, if any + o Process the operands in execution order, if any o Perform required postorder morphing o Perform optional postorder morphing if optimizing */ @@ -6990,6 +7023,9 @@ GenTree* Compiler::fgMorphSmpOp(GenTree* tree, bool* optAssertionPropDone) GenTree* op1 = tree->AsOp()->gtOp1; GenTree* op2 = tree->gtGetOp2IfPresent(); + GenTree** firstUse = nullptr; + GenTree** secondUse = nullptr; + /*------------------------------------------------------------------------- * First do any PRE-ORDER processing */ @@ -7584,11 +7620,19 @@ GenTree* Compiler::fgMorphSmpOp(GenTree* tree, bool* optAssertionPropDone) return fgMorphTree(morphed); } + firstUse = (op1 != nullptr) ? &tree->AsOp()->gtOp1 : nullptr; + secondUse = (op2 != nullptr) ? &tree->AsOp()->gtOp2 : nullptr; + + if (tree->IsReverseOp()) + { + std::swap(firstUse, secondUse); + } + /*------------------------------------------------------------------------- * Process the first operand, if any */ - if (op1 != nullptr) + if (firstUse != nullptr) { // If we are entering the "then" part of a Qmark-Colon we must // save the state of the current assertions table so that we can @@ -7606,7 +7650,7 @@ GenTree* Compiler::fgMorphSmpOp(GenTree* tree, bool* optAssertionPropDone) fgMarkAddrModeForFieldAddr(tree->AsIndir()); } - tree->AsOp()->gtOp1 = op1 = fgMorphTree(op1); + *firstUse = fgMorphTree(*firstUse); // If we are exiting the "then" part of a Qmark-Colon we must // save the state of the current assertions table so that we @@ -7622,7 +7666,7 @@ GenTree* Compiler::fgMorphSmpOp(GenTree* tree, bool* optAssertionPropDone) * Process the second operand, if any */ - if (op2 != nullptr) + if (secondUse != nullptr) { // If we are entering the "else" part of a Qmark-Colon we must // reset the state of the current assertions table @@ -7632,7 +7676,7 @@ GenTree* Compiler::fgMorphSmpOp(GenTree* tree, bool* optAssertionPropDone) BitVecOps::Assign(apTraits, apLocal, origAssertions); } - tree->AsOp()->gtOp2 = op2 = fgMorphTree(op2); + *secondUse = fgMorphTree(*secondUse); // If we are exiting the "else" part of a Qmark-Colon we must // merge the state of the current assertions table with that @@ -7648,6 +7692,17 @@ GenTree* Compiler::fgMorphSmpOp(GenTree* tree, bool* optAssertionPropDone) } } + // Preorder processing and the operand walk may replace operands. Postorder + // transforms use their physical positions, not their execution order. + if (op1 != nullptr) + { + op1 = tree->AsOp()->gtOp1; + } + if (op2 != nullptr) + { + op2 = tree->gtGetOp2IfPresent(); + } + #if !defined(TARGET_64BIT) && !defined(TARGET_WASM) DONE_MORPHING_CHILDREN: #endif // !defined(TARGET_64BIT) && !defined(TARGET_WASM) @@ -7889,6 +7944,7 @@ GenTree* Compiler::fgMorphSmpOp(GenTree* tree, bool* optAssertionPropDone) if (op1->IsIntegralConst(0)) { tree->ChangeOper(GT_NEG, GenTree::PRESERVE_VN); + tree->ClearReverseOp(); tree->gtType = genActualType(op2->TypeGet()); tree->AsOp()->gtOp1 = op2; @@ -7920,7 +7976,7 @@ GenTree* Compiler::fgMorphSmpOp(GenTree* tree, bool* optAssertionPropDone) break; } - if (op1->OperIs(GT_NEG) && gtCanSwapOrder(op1, op2)) + if (op1->OperIs(GT_NEG) && (tree->IsReverseOp() ? gtCanSwapOrder(op2, op1) : gtCanSwapOrder(op1, op2))) { // -a - -b = > b - a // SUB(NEG(a), NEG(b)) => SUB(b, a) @@ -8347,6 +8403,11 @@ GenTree* Compiler::fgMorphSmpOp(GenTree* tree, bool* optAssertionPropDone) // codegen won't like it if op1 is a RELOP of longs, floats or doubles. // So we change it into a GT_COMMA as well. JITDUMP("Also bashing [%06d] (a relop) into a GT_COMMA.\n", dspTreeID(op1)); + if (op1->IsReverseOp()) + { + std::swap(op1->AsOp()->gtOp1, op1->AsOp()->gtOp2); + op1->ClearReverseOp(); + } op1->ChangeOper(GT_COMMA); op1->ClearUnsigned(); // Clear the unsigned flag if it was set on the relop op1->gtType = op1->AsOp()->gtOp1->gtType; @@ -8407,18 +8468,26 @@ GenTree* Compiler::fgMorphSmpOp(GenTree* tree, bool* optAssertionPropDone) if (fgGlobalMorph && (oper != GT_COLON) && /* TODO-ASG-Cleanup: delete this zero-diff quirk */ !GenTree::OperIsStore(oper)) { - if ((op1 != nullptr) && fgIsCommaThrow(op1, true)) + GenTree* firstOp = op1; + GenTree* secondOp = op2; + + if (tree->IsReverseOp()) { - GenTree* propagatedThrow = fgPropagateCommaThrow(tree, op1->AsOp(), GTF_EMPTY); + std::swap(firstOp, secondOp); + } + + if ((firstOp != nullptr) && fgIsCommaThrow(firstOp, true)) + { + GenTree* propagatedThrow = fgPropagateCommaThrow(tree, firstOp->AsOp(), GTF_EMPTY); if (propagatedThrow != nullptr) { return propagatedThrow; } } - if ((op2 != nullptr) && fgIsCommaThrow(op2, true)) + if ((secondOp != nullptr) && fgIsCommaThrow(secondOp, true)) { - GenTree* propagatedThrow = fgPropagateCommaThrow(tree, op2->AsOp(), op1->gtFlags & GTF_ALL_EFFECT); + GenTree* propagatedThrow = fgPropagateCommaThrow(tree, secondOp->AsOp(), firstOp->gtFlags & GTF_ALL_EFFECT); if (propagatedThrow != nullptr) { return propagatedThrow; @@ -9013,6 +9082,9 @@ GenTree* Compiler::fgOptimizeEqualityComparisonWithConst(GenTreeOp* cmp) rshiftOp->gtOp1 = andOp->gtGetOp2(); andOp->gtOp2 = rshiftOp; + // The AND now controls the execution order between x and y. + andOp->gtFlags = (andOp->gtFlags & ~GTF_REVERSE_OPS) | (rshiftOp->gtFlags & GTF_REVERSE_OPS); + rshiftOp->ClearReverseOp(); rshiftOp->SetOper(GT_LSH); gtUpdateNodeSideEffects(rshiftOp); } @@ -9637,7 +9709,8 @@ GenTree* Compiler::fgOptimizeHWIntrinsic(GenTreeHWIntrinsic* node) break; } - if (!gtCanSwapOrder(op1, op2)) + bool canSwap = node->IsReverseOp() ? gtCanSwapOrder(op2, op1) : gtCanSwapOrder(op1, op2); + if (!canSwap) { break; } @@ -10258,7 +10331,8 @@ GenTree* Compiler::fgOptimizeHWIntrinsic(GenTreeHWIntrinsic* node) var_types op1SimdBaseType = op1Intrin->GetSimdBaseType(); if ((op1Oper == GT_NEG) && !op1IsScalar && - (varTypeToSigned(simdBaseType) == varTypeToSigned(op1SimdBaseType))) + (varTypeToSigned(simdBaseType) == varTypeToSigned(op1SimdBaseType)) && + (node->IsReverseOp() ? gtCanSwapOrder(op2, op1) : gtCanSwapOrder(op1, op2))) { op1 = ExtractEffectiveOp(GT_NEG, op1Intrin, /* destroyNodes */ true); @@ -10558,6 +10632,9 @@ GenTree* Compiler::fgOptimizeAddition(GenTreeOp* add) // addOne is now "x + y" addOne->gtOp2 = addTwo->gtGetOp1(); addOne->SetAllEffectsFlags(addOne->gtGetOp1(), addOne->gtGetOp2()); + // The inner add now controls the execution order between x and y. + addOne->gtFlags = (addOne->gtFlags & ~GTF_REVERSE_OPS) | (add->gtFlags & GTF_REVERSE_OPS); + add->ClearReverseOp(); // addTwo is now "icon1 + icon2" so we can fold it using gtFoldExprConst addTwo->gtOp1 = constOne; @@ -10652,7 +10729,7 @@ GenTree* Compiler::fgOptimizeAddition(GenTreeOp* add) // - a + b => b - a // ADD(NEG(a), b) => SUB(b, a) // Do not do this if "op2" is constant for canonicalization purposes. - if (!op2->IsIntegralConst() && gtCanSwapOrder(op1, op2)) + if (!op2->IsIntegralConst() && (add->IsReverseOp() ? gtCanSwapOrder(op2, op1) : gtCanSwapOrder(op1, op2))) { add->SetOper(GT_SUB); add->gtOp1 = op2; @@ -10754,6 +10831,7 @@ GenTree* Compiler::fgOptimizeMultiply(GenTreeOp* mul) else { mul->ChangeOper(GT_NEG, GenTree::PRESERVE_VN); + mul->ClearReverseOp(); mul->AsOp()->gtOp2 = nullptr; return mul; } @@ -10823,6 +10901,7 @@ GenTree* Compiler::fgOptimizeMultiply(GenTreeOp* mul) // We need to keep op1 for the side-effects. Hang it off a GT_COMMA node. mul->ChangeOper(GT_COMMA, GenTree::PRESERVE_VN); + mul->ClearReverseOp(); return mul; } @@ -11084,6 +11163,7 @@ GenTree* Compiler::fgOptimizeBitwiseXor(GenTreeOp* xorOp) { /* "x ^ -1" is "~x" */ xorOp->ChangeOper(GT_NOT, GenTree::PRESERVE_VN); + xorOp->ClearReverseOp(); xorOp->gtOp2 = nullptr; DEBUG_DESTROY_NODE(op2); @@ -11103,6 +11183,7 @@ GenTree* Compiler::fgOptimizeBitwiseXor(GenTreeOp* xorOp) // "x ^ -0.0" is "-x" xorOp->ChangeOper(GT_NEG, GenTree::PRESERVE_VN); + xorOp->ClearReverseOp(); xorOp->gtOp2 = nullptr; DEBUG_DESTROY_NODE(op2); @@ -11506,7 +11587,7 @@ GenTree* Compiler::fgMorphHWIntrinsic(GenTreeHWIntrinsic* tree) { // It is important that this follows the general flow of fgMorphSmpOp // * Perform required preorder processing - // * Process the operands, in order, if any + // * Process the operands, in execution order, if any // * Perform required postorder morphing // * Perform optional postorder morphing if optimizing // @@ -11539,7 +11620,8 @@ GenTree* Compiler::fgMorphHWIntrinsic(GenTreeHWIntrinsic* tree) // Process the operands, if any // - for (GenTree** use : tree->UseEdges()) + // GenTreeMultiOp::UseEdges ignores GTF_REVERSE_OPS; use the execution-order iterator. + for (GenTree** use : tree->GenTree::UseEdges()) { *use = fgMorphTree(*use); GenTree* operand = *use; @@ -12528,6 +12610,9 @@ GenTree* Compiler::fgRecognizeAndMorphBitwiseRotation(GenTree* tree) { noway_assert(GenTree::OperIsRotate(rotateOp)); + // Preserve the first-executed shift's operand order. The parent's flag only orders the two shifts. + GenTreeFlags operandOrder = (tree->IsReverseOp() ? op2 : op1)->gtFlags & GTF_REVERSE_OPS; + // Explicitly mask the rotate amount to the range [0, bitsize-1]. Otherwise, a later // transform can stick an out of range constant here and trip up lowering. If the // target's rotate or shift instructions mask their operand implicitly, those targets @@ -12573,6 +12658,7 @@ GenTree* Compiler::fgRecognizeAndMorphBitwiseRotation(GenTree* tree) noway_assert(inputTreeEffects == (tree->gtFlags & GTF_ALL_EFFECT)); } + tree->gtFlags = (tree->gtFlags & ~GTF_REVERSE_OPS) | operandOrder; return tree; } } @@ -12649,14 +12735,15 @@ GenTreeOp* Compiler::fgMorphLongMul(GenTreeOp* mul) GenTree* op1 = mul->gtGetOp1(); GenTree* op2 = mul->gtGetOp2(); - // Morph the operands. We cannot allow the casts to go away, so we morph their operands directly. - op1->AsCast()->CastOp() = fgMorphTree(op1->AsCast()->CastOp()); - op1->SetAllEffectsFlags(op1->AsCast()->CastOp()); - - if (op2->OperIs(GT_CAST)) + // Morph in execution order. We cannot allow the casts to go away, + // so we morph their operands directly. + for (GenTree* operand : mul->Operands()) { - op2->AsCast()->CastOp() = fgMorphTree(op2->AsCast()->CastOp()); - op2->SetAllEffectsFlags(op2->AsCast()->CastOp()); + if (operand->OperIs(GT_CAST)) + { + operand->AsCast()->CastOp() = fgMorphTree(operand->AsCast()->CastOp()); + operand->SetAllEffectsFlags(operand->AsCast()->CastOp()); + } } mul->SetAllEffectsFlags(op1, op2); diff --git a/src/tests/JIT/Regression_ro_2/Runtime_134334.cs b/src/tests/JIT/Regression_ro_2/Runtime_134334.cs new file mode 100644 index 00000000000000..5cb2f0874d50ed --- /dev/null +++ b/src/tests/JIT/Regression_ro_2/Runtime_134334.cs @@ -0,0 +1,188 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using Xunit; + +public class Runtime_134334 +{ + private static int s_calls; + + private sealed class Receiver + { + public int Field = 3; + + [MethodImpl(MethodImplOptions.NoInlining)] + public int GetValue() + { + s_calls++; + return 7; + } + } + + [Theory] + [InlineData(128)] + [InlineData(256)] + [InlineData(512)] + public static void SimdOperands(int width) + { + s_calls = 0; + Assert.Throws(() => Concat(null, width)); + Assert.Equal(0, s_calls); + + Assert.Equal(73, Concat(new Receiver(), width)); + Assert.Equal(1, s_calls); + } + + [Theory] + [InlineData(true, 7)] + [InlineData(false, 10)] + public static void ScalarOperands(bool store, int expected) + { + s_calls = 0; + Assert.Throws(() => Scalar(null, store)); + Assert.Equal(0, s_calls); + + var receiver = new Receiver(); + Assert.Equal(expected, Scalar(receiver, store)); + Assert.Equal(1, s_calls); + } + + public enum ScalarRewrite + { + SubtractNegated, + AddNegated, + AddConstants, + ShiftRight, + ShiftRightUnsigned, + RotateLeft, + } + + [Theory] + [InlineData(ScalarRewrite.SubtractNegated, -2)] + [InlineData(ScalarRewrite.AddNegated, -2)] + [InlineData(ScalarRewrite.AddConstants, 28)] + [InlineData(ScalarRewrite.ShiftRight, 0)] + [InlineData(ScalarRewrite.ShiftRightUnsigned, 0)] + [InlineData(ScalarRewrite.RotateLeft, 6)] + public static void ScalarRewrites(ScalarRewrite operation, int expected) + { + Assert.Throws(() => Rewrite(null, 0, operation)); + Assert.Throws(() => Rewrite(new int[3], 0, operation)); + Assert.Equal(expected, Rewrite(new int[3], 1, operation)); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static int Rewrite(int[] values, int divisor, ScalarRewrite operation) + { + return operation switch + { + ScalarRewrite.SubtractNegated => -values.Length - -(1 / divisor), + ScalarRewrite.AddNegated => -values.Length + (1 / divisor), + ScalarRewrite.AddConstants => (values.Length + 11) + ((1 / divisor) + 13), + ScalarRewrite.ShiftRight => ((values.Length >> (1 / divisor)) & 1) == 0 ? 1 : 0, + ScalarRewrite.ShiftRightUnsigned => ((values.Length >>> (1 / divisor)) & 1) == 0 ? 1 : 0, + ScalarRewrite.RotateLeft => (values.Length << (1 / divisor)) | (values.Length >>> (32 - (1 / divisor))), + _ => throw new ArgumentOutOfRangeException(nameof(operation)), + }; + } + + [Theory] + [InlineData(128)] + [InlineData(256)] + [InlineData(512)] + public static void SimdNegationOperands(int width) + { + Assert.Throws(() => SubtractNegated(Array.Empty(), null, width)); + Assert.Throws(() => SubtractNegated(Array.Empty(), null, width)); + Assert.Throws(() => SubtractNegated(Array.Empty(), null, width)); + Assert.Equal(4, SubtractNegated(new[] { 3 }, new[] { 7 }, width)); + } + + private static T SubtractNegated(T[] first, T[] second, int width) where T : unmanaged + { + return width switch + { + 128 => SubtractNegated128(first, second), + 256 => SubtractNegated256(first, second), + 512 => SubtractNegated512(first, second), + _ => throw new ArgumentOutOfRangeException(nameof(width)), + }; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static T SubtractNegated128(T[] first, T[] second) where T : unmanaged + { + Vector128 result = -Vector128.Create(first[0]) - -Vector128.Create(second[0]); + return result[0]; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static T SubtractNegated256(T[] first, T[] second) where T : unmanaged + { + Vector256 result = -Vector256.Create(first[0]) - -Vector256.Create(second[0]); + return result[0]; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static T SubtractNegated512(T[] first, T[] second) where T : unmanaged + { + Vector512 result = -Vector512.Create(first[0]) - -Vector512.Create(second[0]); + return result[0]; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static int Scalar(Receiver receiver, bool store) + { + if (store) + { + receiver.Field = receiver.GetValue(); + return receiver.Field; + } + + return Subtract(receiver, 20); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static int Subtract(Receiver receiver, int value) + { + return value - (receiver.GetValue() + receiver.Field); + } + + private static int Concat(Receiver receiver, int width) + { + return width switch + { + 128 => Concat128(receiver), + 256 => Concat256(receiver), + 512 => Concat512(receiver), + _ => throw new ArgumentOutOfRangeException(nameof(width)), + }; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static int Concat128(Receiver receiver) + { + Vector128 result = Vector128.ConcatUpperUpper( + Vector128.Create(receiver.GetValue()), Vector128.Create(receiver.Field)); + return result[0] * 10 + result[Vector128.Count / 2]; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static int Concat256(Receiver receiver) + { + Vector256 result = Vector256.ConcatUpperUpper( + Vector256.Create(receiver.GetValue()), Vector256.Create(receiver.Field)); + return result[0] * 10 + result[Vector256.Count / 2]; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static int Concat512(Receiver receiver) + { + Vector512 result = Vector512.ConcatUpperUpper( + Vector512.Create(receiver.GetValue()), Vector512.Create(receiver.Field)); + return result[0] * 10 + result[Vector512.Count / 2]; + } +} From f86cfe9086f2a702286b475a47f57b89d58520be Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Wed, 23 Sep 2026 13:27:04 -0700 Subject: [PATCH 2/2] Preserve null checks when Mono resolves callvirt directly Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/mono/mono/mini/method-to-ir.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/mono/mono/mini/method-to-ir.c b/src/mono/mono/mini/method-to-ir.c index 5e3a689098fe44..32edc458fa013c 100644 --- a/src/mono/mono/mini/method-to-ir.c +++ b/src/mono/mono/mini/method-to-ir.c @@ -7894,6 +7894,7 @@ mono_method_to_ir (MonoCompile *cfg, MonoMethod *method, MonoBasicBlock *start_b if (cmethod_override) { cmethod = cmethod_override; cmethod_override = NULL; + check_this = virtual_; virtual_ = FALSE; //il_op = MONO_CEE_CALL; } else { @@ -8054,6 +8055,12 @@ mono_method_to_ir (MonoCompile *cfg, MonoMethod *method, MonoBasicBlock *start_b sp -= n; + // A resolved callvirt needs its null check even when the target is inlined. + if (check_this) { + MONO_EMIT_NEW_CHECK_THIS (cfg, sp [0]->dreg); + check_this = FALSE; + } + if (virtual_ && cmethod && sp [0] && sp [0]->opcode == OP_TYPED_OBJREF) { error_init_reuse (error); MonoMethod *new_cmethod = mono_class_get_virtual_method (sp [0]->klass, cmethod, error);