From 01707f3f0817aafa74b5949926148009543e2535 Mon Sep 17 00:00:00 2001 From: Andy Ayers Date: Tue, 15 Sep 2026 13:32:57 -0700 Subject: [PATCH] JIT: Attribute GDV inline failures to candidate GDV inline checks reported failures against the original virtual method, permanently marking the wrong method as not inlineable. Record the guarded candidate as the callee and add regression coverage. Fixes #133974 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: cc9ff2b1-3240-4db8-85b3-8e42b2a0f2ec --- src/coreclr/jit/importercalls.cpp | 9 ++- src/coreclr/jit/inline.cpp | 15 ++-- src/coreclr/jit/inline.h | 8 +- .../opt/GuardedDevirtualization/badinlinee.cs | 75 +++++++++++++++++++ .../GuardedDevirtualization/badinlinee.csproj | 15 ++++ 5 files changed, 114 insertions(+), 8 deletions(-) create mode 100644 src/tests/JIT/opt/GuardedDevirtualization/badinlinee.cs create mode 100644 src/tests/JIT/opt/GuardedDevirtualization/badinlinee.csproj diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index 7b849454fc00ff..b35d009f20ef58 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -9224,7 +9224,14 @@ void Compiler::impMarkInlineCandidate(GenTree* callNode, for (uint8_t candidateId = 0; candidateId < call->GetInlineCandidatesCount(); candidateId++) { - InlineResult inlineResult(this, call, nullptr, "impMarkInlineCandidate for GDV"); + InlineCandidateInfo* gdvCandidate = call->GetGDVCandidateInfo(candidateId); + CORINFO_METHOD_HANDLE callee = gdvCandidate->guardedMethodUnboxedResolvedToken.hMethod; + if (callee == nullptr) + { + callee = gdvCandidate->guardedMethodHandle; + } + + InlineResult inlineResult(this, call, nullptr, "impMarkInlineCandidate for GDV", false, callee); // Do the actual evaluation impMarkInlineCandidateHelper(call, candidateId, exactContextHnd, callInfo, inlinersContext, &inlineResult); diff --git a/src/coreclr/jit/inline.cpp b/src/coreclr/jit/inline.cpp index 91535236bc27f5..150fa17a8740da 100644 --- a/src/coreclr/jit/inline.cpp +++ b/src/coreclr/jit/inline.cpp @@ -640,15 +640,20 @@ void InlineContext::DumpXml(FILE* file, unsigned indent) // call - the call in question // stmt - statement containing the call (if known) // description - string describing the context of the decision - -InlineResult::InlineResult( - Compiler* compiler, GenTreeCall* call, Statement* stmt, const char* description, bool doNotReport) +// callee - the actual inline candidate, if different from the call target + +InlineResult::InlineResult(Compiler* compiler, + GenTreeCall* call, + Statement* stmt, + const char* description, + bool doNotReport, + CORINFO_METHOD_HANDLE callee) : m_RootCompiler(nullptr) , m_Policy(nullptr) , m_Call(call) , m_InlineContext(nullptr) , m_Caller(nullptr) - , m_Callee(nullptr) + , m_Callee(callee) , m_ImportedILSize(0) , m_Description(description) , m_successResult(INLINE_PASS) @@ -689,7 +694,7 @@ InlineResult::InlineResult( m_Caller = compiler->info.compMethodHnd; // Get method handle for callee, if known - if (m_Call->AsCall()->gtCallType == CT_USER_FUNC) + if ((m_Callee == nullptr) && (m_Call->AsCall()->gtCallType == CT_USER_FUNC)) { m_Callee = m_Call->AsCall()->gtCallMethHnd; } diff --git a/src/coreclr/jit/inline.h b/src/coreclr/jit/inline.h index 28452a567fb1de..ef1f93c45235c7 100644 --- a/src/coreclr/jit/inline.h +++ b/src/coreclr/jit/inline.h @@ -344,8 +344,12 @@ class InlineResult public: // Construct a new InlineResult to help evaluate a // particular call for inlining. - InlineResult( - Compiler* compiler, GenTreeCall* call, Statement* stmt, const char* description, bool doNotReport = false); + InlineResult(Compiler* compiler, + GenTreeCall* call, + Statement* stmt, + const char* description, + bool doNotReport = false, + CORINFO_METHOD_HANDLE callee = nullptr); // Construct a new InlineResult to evaluate a particular // method to see if it is inlineable. diff --git a/src/tests/JIT/opt/GuardedDevirtualization/badinlinee.cs b/src/tests/JIT/opt/GuardedDevirtualization/badinlinee.cs new file mode 100644 index 00000000000000..8dbdc9195bcc71 --- /dev/null +++ b/src/tests/JIT/opt/GuardedDevirtualization/badinlinee.cs @@ -0,0 +1,75 @@ +// 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.Threading; +using Xunit; + +public class BadInlinee +{ + private class Base + { + public virtual int M() => 42; + } + + private sealed class Derived : Base + { + [MethodImpl(MethodImplOptions.Synchronized)] + public override int M() => 43; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static int Poison(Base value) => value.M(); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static int Test() + { + Base value = new Base(); + return value.M(); + } + + [Fact] + public static int TestEntryPoint() + { + Base value = new Derived(); + int result = 0; + + for (int i = 0; i < 100; i++) + { + for (int j = 0; j < 100; j++) + { + result += Poison(value); + } + + Thread.Sleep(15); + } + + for (int i = 0; i < 100; i++) + { + for (int j = 0; j < 100; j++) + { + result += Test(); + } + + Thread.Sleep(15); + } + + long allocatedBytesBefore = GC.GetAllocatedBytesForCurrentThread(); + + for (int i = 0; i < 1_000; i++) + { + result += Test(); + } + + long allocatedBytes = GC.GetAllocatedBytesForCurrentThread() - allocatedBytesBefore; + + if (allocatedBytes != 0) + { + Console.WriteLine($"Test allocated {allocatedBytes} bytes"); + return -1; + } + + return result == 892_000 ? 100 : -1; + } +} diff --git a/src/tests/JIT/opt/GuardedDevirtualization/badinlinee.csproj b/src/tests/JIT/opt/GuardedDevirtualization/badinlinee.csproj new file mode 100644 index 00000000000000..5d2eec4b7dbfae --- /dev/null +++ b/src/tests/JIT/opt/GuardedDevirtualization/badinlinee.csproj @@ -0,0 +1,15 @@ + + + 1 + True + true + + + + + + + + + +