Skip to content

JIT: add NativeAOT support for non-shared GVM devirtualization#130202

Open
hez2010 wants to merge 11 commits into
dotnet:mainfrom
hez2010:gvm-devirt-nativeaot
Open

JIT: add NativeAOT support for non-shared GVM devirtualization#130202
hez2010 wants to merge 11 commits into
dotnet:mainfrom
hez2010:gvm-devirt-nativeaot

Conversation

@hez2010

@hez2010 hez2010 commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Under NativeAOT, only mark the fat pointer calls but defer the transformation until we are going to perform indirect calls transform, so that the JIT can recognize the tree for GVM devirtualization.

Unfortunately, we are not able to support late devirtualization and guarded devirtualization because fat pointer call transformation happens too early.

Shared GVMs are also not supported due to IL scanner limitations.

Example:

interface IGvm
{
    void Test<T>(T value);
}

class Gvm : IGvm
{
    public void Test<T>(T value)
    {
        Console.WriteLine(value);
    }
}

class Program
{
    static void Main(string[] args)
    {
        IGvm gvm1 = new Gvm();
        gvm1.Test(42);
    }
}

Before:

G_M24006_IG01:
       push     rbx
       sub      rsp, 32
G_M24006_IG02:
       lea      rcx, [(reloc 0x420b30)]      ; Gvm
       call     CORINFO_HELP_NEWSFAST
       mov      rbx, rax
       mov      rcx, rbx
       lea      rdx, [(reloc 0x420b40)]      ; IGvm:Test[int](int):this
       call     CORINFO_HELP_GVMLOOKUP_FOR_SLOT
       test     al, 2
       je       SHORT G_M24006_IG04
G_M24006_IG03:
       mov      rdx, qword ptr [rax+0x06]
       mov      rcx, rbx
       mov      r8d, 42
       call     [rax-0x02]
       jmp      SHORT G_M24006_IG05
G_M24006_IG04:
       mov      rcx, rbx
       mov      edx, 42
       call     rax
G_M24006_IG05:
       nop
G_M24006_IG06:
       add      rsp, 32
       pop      rbx
       ret

After:

G_M24006_IG01:
       sub      rsp, 40
G_M24006_IG02:
       lea      rcx, [(reloc 0x420bc0)]      ; System.Int32
       call     CORINFO_HELP_NEWSFAST
       mov      dword ptr [rax+0x08], 42
       mov      rcx, rax
       call     System.Console:WriteLine(System.Object)
       nop
G_M24006_IG03:
       add      rsp, 40
       ret

Contributes to #112596

Copilot AI review requested due to automatic review settings July 4, 2026 15:25
@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 Jul 4, 2026
@dotnet-policy-service dotnet-policy-service Bot added the community-contribution Indicates that the PR has been added by a community member label Jul 4, 2026
@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

Enables NativeAOT generic virtual method (GVM) devirtualization by preserving the helper-call-shaped indirection target long enough for the importer’s devirtualization logic to recognize it, while still supporting NativeAOT fat-pointer lowering later during indirect-call transformation.

Changes:

  • Stop spilling NativeAOT GVM ldvirtftn targets to a temp in the importer; instead only mark calls as fat-pointer candidates.
  • In indirect-call transformation, for generic-virtual fat-pointer candidates, split/spill as needed and then materialize the function pointer into a temp local before expanding the fat-pointer control-flow.
  • Ensure fat-pointer-candidate marking is cleared when a call is devirtualized to a direct call; additionally, skip guarded devirtualization for generic-virtual calls.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/coreclr/jit/indirectcalltransformer.cpp Adds late temp materialization of GVM fat-pointer call targets (post-import) prior to fat-pointer expansion.
src/coreclr/jit/importercalls.cpp Removes early temp spill for NativeAOT GVM ldvirtftn calls; blocks GDV for generic-virtual calls; clears fat-pointer candidate on devirtualization.

Comment thread src/coreclr/jit/indirectcalltransformer.cpp Outdated
Copilot AI review requested due to automatic review settings July 4, 2026 15:54
@hez2010 hez2010 requested a review from MichalStrehovsky as a code owner July 4, 2026 15:54

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 3 out of 3 changed files in this pull request and generated 2 comments.

Comment thread src/coreclr/jit/indirectcalltransformer.cpp
Comment thread src/coreclr/jit/importercalls.cpp Outdated
Copilot AI review requested due to automatic review settings July 4, 2026 16:02

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 3 out of 3 changed files in this pull request and generated 1 comment.

Comment thread src/coreclr/jit/importercalls.cpp Outdated
@hez2010

hez2010 commented Jul 4, 2026

Copy link
Copy Markdown
Contributor Author

It's hitting IL scanner issues like

error : VTable of type 'DerivedFtnn`1<System.Object>' not computed by the IL scanner. You can work around by running the compilation with scanner disabled. 

cc: @MichalStrehovsky does this ring a bell?

UPDATE: addressed in 2291537

Copilot AI review requested due to automatic review settings July 5, 2026 04:24

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 5 out of 5 changed files in this pull request and generated 2 comments.

Comment thread src/coreclr/jit/indirectcalltransformer.cpp Outdated
Comment thread src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/ILScanner.cs Outdated
Copilot AI review requested due to automatic review settings July 5, 2026 04:30

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 5 out of 5 changed files in this pull request and generated 2 comments.

Comment thread src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/ILScanner.cs Outdated
Comment thread src/coreclr/jit/indirectcalltransformer.cpp Outdated
Copilot AI review requested due to automatic review settings July 5, 2026 04:39

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 5 out of 5 changed files in this pull request and generated 3 comments.

Comment thread src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/ILScanner.cs Outdated
Comment thread src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/ILScanner.cs Outdated
Comment thread src/coreclr/jit/indirectcalltransformer.cpp Outdated
@MichalStrehovsky

Copy link
Copy Markdown
Member

The scanner change and Program.cs are working around a legit issue (the tests are still failing because of that).

The whole program analysis of GVMs currently assumes the call will be lazy and will go through the type loader. The thing that is failing now is following test:

    class TestGvmDependencies
    {
        class Atom { }

        class Foo
        {
            public virtual object Frob<T>()
            {
                return new T[0, 0];
            }
        }

        class Bar : Foo
        {
            public override object Frob<T>()
            {
                return new T[0, 0, 0];
            }
        }

        public static void Run()
        {
            {
                Foo x = new Foo();
                x.Frob<Atom>();
            }

            {
                Foo x = new Bar();
                x.Frob<Atom>();
            }
        }
    }

We don't expect Atom[,] to exist during scanning because GVM analysis only ever sees Frob<__Canon> (we deliberately ignore the Frob<Atom> call and consider it just Frob<__Canon>). We do this for two reasons - compile speed, and size. GVMs expand very aggressively and not precomputing all the types that we could build lazily saves us both.

We could in theory have a mode that does the full analysis and analyzes Frob<Atom> that then forces both Atom[,] and Atom[,,] to exists, but we don't have it yet.

I think this will have to be limited to struct instantiations only for now. We could allow reference types if the other analysis mode is added.

This is one of the things where RyuJIT-as-IL-scanner would help because then we would do this devirt during scanning already and GVM analysis never gets in the picture (it's not considered a GVM call in the first place).

@hez2010 hez2010 changed the title JIT: add NativeAOT support for GVM devirtualization JIT: add NativeAOT support for non-shared GVM devirtualization Jul 6, 2026
@hez2010 hez2010 marked this pull request as ready for review July 6, 2026 12:11
Copilot AI review requested due to automatic review settings July 6, 2026 12:11

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@hez2010

hez2010 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Bail the shared GVM devirt support for NativeAOT out right now....

@hez2010

hez2010 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

It becomes more like a JIT change now. @jakobbotsch could you please take a look?

Comment on lines +329 to +343
if (m_origCall->IsGenericVirtual(m_compiler) && m_origCall->IsFatPointerCandidate())
{
Statement* firstNewStmt = nullptr;
GenTree** splitPointInUse = nullptr;

m_compiler->gtSplitTree(block, stmt, m_origCall->gtControlExpr, &firstNewStmt, &splitPointInUse, true);
const unsigned fptrLclNum = m_compiler->lvaGrabTemp(true DEBUGARG("fat pointer temp"));
GenTree* const store = m_compiler->gtNewTempStore(fptrLclNum, m_origCall->gtControlExpr);
Statement* const storeStmt = m_compiler->gtNewStmt(store, stmt->GetDebugInfo());
m_compiler->fgInsertStmtBefore(block, stmt, storeStmt);
m_origCall->gtControlExpr =
m_compiler->gtNewLclvNode(fptrLclNum, genActualType(m_origCall->gtControlExpr->TypeGet()));
m_compiler->gtUpdateStmtSideEffects(storeStmt);
m_compiler->gtUpdateStmtSideEffects(stmt);
}

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 looks odd to do in the constructor. Override Run and do it from there instead?

m_compiler->gtUpdateStmtSideEffects(storeStmt); looks unnecessary.

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.

Actually, you can do this in the same way as GuardedDevirtualizationTransformer does it in CreateCheck. This code can probably be shared between the two.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Moved it to Run instead like GuardedDevirtualizationTransformer.

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 community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants