JIT: (bug) Rejecting a GDV candidate permanently marks the base virtual method as NOINLINE process-wide
- 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
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