Don't require GDV targets to be inlineable - #132375
Conversation
Guarded devirtualization used to give up whenever the target we'd devirtualize to couldn't be inlined. But a direct call is still cheaper than a virtual or interface call, so keep the candidate and just don't inline it. Added JitGuardedDevirtualizationRequireInlining to get the old behavior back. Class-based GDV only for now, method/delegate GDV is unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 38498b8d-c116-4637-b069-d1d275634193
|
Azure Pipelines: Successfully started running 5 pipeline(s). 11 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
There was a problem hiding this comment.
Pull request overview
This PR adjusts CoreCLR JIT guarded devirtualization (GDV) so that class-based GDV candidates can be kept and expanded even when the devirtualized target isn’t inlineable, preserving the direct-call benefit. It also adds a config switch to restore the legacy “require inlineability” behavior and tightens up candidate bookkeeping to support the new flow.
Changes:
- Add
JitGuardedDevirtualizationRequireInliningconfig (default0) to optionally restore legacy behavior of dropping non-inlineable GDV targets. - Track per-candidate inlineability via
InlineCandidateInfo::isInlineableand keep non-inlineable class-GDV candidates for devirtualization-only. - Fix candidate bookkeeping (value-initialize
InlineCandidateInfoand adjust spill-temp/enumerator-cloning mapping handling) to avoid relying onimpCheckCanInlinehaving run.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/coreclr/jit/jitmetadatalist.h | Adds a new JIT metric counter (NoInlineGDV) for devirt-without-inlining cases. |
| src/coreclr/jit/jitconfigvalues.h | Introduces JitGuardedDevirtualizationRequireInlining config to gate legacy behavior. |
| src/coreclr/jit/inline.h | Adds InlineCandidateInfo::isInlineable to distinguish inline vs devirt-only GDV candidates. |
| src/coreclr/jit/indirectcalltransformer.cpp | Removes “must be inline-candidate” bail-out; expands GDV while conditionally re-marking inline candidates; fixes spill-temp and enumerator cloning map behavior. |
| src/coreclr/jit/importercalls.cpp | Keeps non-inlineable class-GDV candidates when legal; value-initializes candidate info; adjusts struct-return fixup deferral for GDV candidates. |
| src/coreclr/jit/compiler.h | Declares Compiler::canKeepNonInlineableGdvCandidate. |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 38498b8d-c116-4637-b069-d1d275634193
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 38498b8d-c116-4637-b069-d1d275634193
|
@MihuBot -nuget |
@MichalPetryka This change requires PGO data, MihuBot is basically just PrepareMethod |
|
/azp run runtime-coreclr pgostress, runtime-coreclr libraries-pgo |
|
Azure Pipelines: Successfully started running 2 pipeline(s). |
|
@EgorBot -linux_amd -osx_arm64 -windows_amd using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
BenchmarkSwitcher.FromAssembly(typeof(Bench).Assembly).Run(args);
public interface IOp { int Do(int x); }
public class OpA : IOp { [MethodImpl(MethodImplOptions.NoInlining)] public int Do(int x) => x + 1; }
public class OpB : IOp { [MethodImpl(MethodImplOptions.NoInlining)] public int Do(int x) => x + 2; }
public abstract class Base { public abstract int Do(int x); }
public class DerA : Base { [MethodImpl(MethodImplOptions.NoInlining)] public override int Do(int x) => x + 1; }
public class DerB : Base { [MethodImpl(MethodImplOptions.NoInlining)] public override int Do(int x) => x + 2; }
public class Bench
{
private IOp[] _ifaces = new IOp[64];
private Base[] _virts = new Base[64];
[GlobalSetup]
public void Setup()
{
for (int i = 0; i < 64; i++)
{
// 75% A, 25% B
_ifaces[i] = i % 4 == 0 ? new OpB() : new OpA();
_virts[i] = i % 4 == 0 ? new DerB() : (Base)new DerA();
}
}
[Benchmark]
public int InterfaceCall()
{
int sum = 0;
IOp[] ops = _ifaces;
for (int i = 0; i < ops.Length; i++)
sum += ops[i].Do(i);
return sum;
}
[Benchmark]
public int VirtualCall()
{
int sum = 0;
Base[] objs = _virts;
for (int i = 0; i < objs.Length; i++)
sum += objs[i].Do(i);
return sum;
}
} |
|
PTAL @AndyAyersMS (see PR description), .NET 12.0 change cc @dotnet/jit-contrib. |
|
cc @MichalStrehovsky I think you asked for it a while back for NAOT. |
| return false; | ||
| } | ||
|
|
||
| // Except recursive ones, where turning the call into a loop is more valuable. |
There was a problem hiding this comment.
This seems a bit odd to me. If we fail to form a GDV we won't be able to do the tail recursion->loop trick either.
| { | ||
| call->RemoveGDVCandidateInfo(this, candidateId); | ||
| candidateId--; | ||
| continue; |
There was a problem hiding this comment.
Keep the revoking dump message like we had before?
GDV used to bail out when the target wasn't inlineable. A direct call still beats a virtual one, so keep the candidate and just don't inline it.
DOTNET_JitGuardedDevirtualizationRequireInlining=1restores the old behavior. Class probes only.I know it doesn't match Andy's numbers, but many things have changed since then (e.g. CET), also, I assume we never tested it on arm64. So far it's a pure improvement on most kinds of scenarious (interface, virtual calls, etc.), see benchmarks below. We still might want to avoid doing it for virtual calls (esp for large target methods where the cost of the type check + direct call is a noise anyway) only on CoreCLR, but I propose we do it for everything and watch dotnet/performance reports.
Should help NativeAOT the most, where
getExactClassesgives exact devirtualization but targets often aren't inlineable (if at least one target ofgetExactClasseswasn't inlineable we used to bail on the whole thing).CoreCLR, default env, all targets
[NoInlining](so baseline does no GDV at all). Columns = how often the guessed type hits, ratio = new/base, lower is better.DirectControl(noise floor)InterfaceInterfaceShared(__Canon)InterfaceCostlyTargetVirtualVirtualShared(__Canon)InterfaceHugeTarget(GDV in both)Only regression is monomorphic shared-generic virtual - a well-predicted vtable call is already as cheap as compare+direct call.
rt-sz report: MichalStrehovsky/rt-sz#246
smoke-test benchmarks: EgorBot/Benchmarks#489 (comment)