Skip to content

Don't require GDV targets to be inlineable - #132375

Open
EgorBo wants to merge 3 commits into
dotnet:mainfrom
EgorBo:gdv-no-inline
Open

Don't require GDV targets to be inlineable#132375
EgorBo wants to merge 3 commits into
dotnet:mainfrom
EgorBo:gdv-no-inline

Conversation

@EgorBo

@EgorBo EgorBo commented Aug 16, 2026

Copy link
Copy Markdown
Member

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=1 restores 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 getExactClasses gives exact devirtualization but targets often aren't inlineable (if at least one target of getExactClasses wasn'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.

Benchmark Mono Hit90 Hit50 Hit33 Hit25
DirectControl (noise floor) 1.01 0.99 1.01 1.01 1.00
Interface $\color{green}{0.72}$ $\color{green}{0.48}$ $\color{green}{0.45}$ $\color{green}{0.64}$ $\color{green}{0.56}$
InterfaceShared (__Canon) $\color{green}{0.88}$ $\color{green}{0.50}$ $\color{green}{0.51}$ $\color{green}{0.62}$ $\color{green}{0.59}$
InterfaceCostlyTarget $\color{green}{0.86}$ $\color{green}{0.58}$ $\color{green}{0.55}$ $\color{green}{0.65}$ $\color{green}{0.60}$
Virtual $\color{green}{0.86}$ $\color{green}{0.78}$ $\color{green}{0.63}$ $\color{green}{0.64}$ $\color{green}{0.62}$
VirtualShared (__Canon) $\color{red}{1.19}$ $\color{green}{0.55}$ $\color{green}{0.55}$ $\color{green}{0.52}$ $\color{green}{0.63}$
InterfaceHugeTarget (GDV in both) 1.10 1.10 1.03 1.01 1.00

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)

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
Copilot AI lite review requested due to automatic review settings August 16, 2026 16:31
@github-actions github-actions Bot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Aug 16, 2026
@azure-pipelines

Copy link
Copy Markdown
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.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 JitGuardedDevirtualizationRequireInlining config (default 0) to optionally restore legacy behavior of dropping non-inlineable GDV targets.
  • Track per-candidate inlineability via InlineCandidateInfo::isInlineable and keep non-inlineable class-GDV candidates for devirtualization-only.
  • Fix candidate bookkeeping (value-initialize InlineCandidateInfo and adjust spill-temp/enumerator-cloning mapping handling) to avoid relying on impCheckCanInline having 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.

Comment thread src/coreclr/jit/importercalls.cpp Outdated
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 38498b8d-c116-4637-b069-d1d275634193
Copilot AI review requested due to automatic review settings August 16, 2026 16:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 38498b8d-c116-4637-b069-d1d275634193
Copilot AI review requested due to automatic review settings August 16, 2026 17:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

@MichalPetryka

Copy link
Copy Markdown
Contributor

@MihuBot -nuget

@EgorBo

EgorBo commented Aug 16, 2026

Copy link
Copy Markdown
Member Author

@MihuBot -nuget

@MichalPetryka This change requires PGO data, MihuBot is basically just PrepareMethod

@EgorBo

EgorBo commented Aug 16, 2026

Copy link
Copy Markdown
Member Author

/azp run runtime-coreclr pgostress, runtime-coreclr libraries-pgo

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 2 pipeline(s).

@EgorBo

EgorBo commented Aug 16, 2026

Copy link
Copy Markdown
Member Author

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

@EgorBo

EgorBo commented Aug 17, 2026

Copy link
Copy Markdown
Member Author

PTAL @AndyAyersMS (see PR description), .NET 12.0 change cc @dotnet/jit-contrib.

@EgorBo
EgorBo requested a review from AndyAyersMS August 17, 2026 16:47
@EgorBo

EgorBo commented Aug 17, 2026

Copy link
Copy Markdown
Member Author

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep the revoking dump message like we had before?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants