dotnet / dotnet/runtime

JIT: (bug) Rejecting a GDV candidate permanently marks the base virtual method as NOINLINE process-wide

Closed
#133,974 1 comment 0 reactions 1 assignee Assigned to @AndyAyersMS View on GitHub
area-CodeGen-coreclr
Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Description

A guarded-devirtualization inline rejection for a synchronized override can set `CORINFO_FLG_BAD_INLINEE` on the base virtual method. Subsequent unrelated exact calls to `Base.M` in the same process lose inlining and allocation removal based on tier-up order.

### Minimal Repro

```csharp
using System;
using System.Runtime.CompilerServices;
using System.Threading;

class Base { public virtual int M() => 42; }
class Derived : Base { [MethodImpl(MethodImplOptions.Synchronized)] public override int M() => 43; }

class Program
{
[MethodImpl(MethodImplOptions.NoInlining)]
static int Poison(Base b) => b.M();

[MethodImpl(MethodImplOptions.NoInlining)]
static int Test()
{
Base b = new Base();
return b.M();
}

static void Main(string[] args)
{
int sum = 0;
if (args.Length == 0)
{
Base d = new Derived();
for (int i = 0; i < 100; i++)
{
for (int j = 0; j < 100; j++) sum += Poison(d);
Thread.Sleep(15);
}
}
for (int i = 0; i < 100; i++)
{
for (int j = 0; j < 100; j++) sum += Test();
Thread.Sleep(15);
}
Console.WriteLine(sum);
}
}
```

Requires `DOTNET_TieredPGO=1`, `DOTNET_TC_CallCountThreshold=1`, `DOTNET_TC_CallCountingDelayMs=0`, and `DOTNET_JitDisasm=Test`; pass any argument for the baseline and no arguments to poison `Base.M` first.

### Expected

`Test()` should still inline `Base.M`, fold the return value, and eliminate the `Base` allocation:

```
; Assembly listing for method Program:Test():int (Tier1)
mov eax, 42
ret
; Total bytes of code 6
420000
```

### Actual

After the separate `Poison` method tiers up first, `Test()` is still devirtualized but no longer inlined:

```
; Assembly listing for method Program:Test():int (Tier1)
call CORINFO_HELP_NEWSFAST
mov rcx, rax
call [Base:M():int:this]
; Total bytes of code 34
850000
```

### Notes

`impMarkInlineCandidate` builds `InlineResult` from the base call node for GDV candidates, so `InlineResult::Report` propagates `CALLEE_*` failures via `setMethodAttribs` to `gtCallMethHnd` (`Base.M`) rather than the guarded target (`Derived.M`).
The effect is process-wide and order-dependent because `CORINFO_FLG_BAD_INLINEE` is a VM-level method attribute.
Reproduces on released .NET 10.0.12 as the same 6-byte vs 34-byte Tier1 codegen difference.

Contributor guide

Open the contributing guide

Research direction

Start with impMarkInlineCandidate and follow how InlineResult::Report propagates CALLEE_* failures through setMethodAttribs to gtCallMethHnd. Use the supplied minimal repro with the listed tiering and JIT settings, comparing Test() after Poison() tiers up; done means rejecting the guarded Derived.M candidate no longer marks Base.M process-wide, and Test() retains the expected inlining and allocation removal.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.