Repository navigation
Closed
Labels
Milestone
Description
Consider the following code:
using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
interface IM<T>
{
bool UseDefaultM { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => true; }
ValueTask M(T instance) => throw new NotImplementedException("M must be implemented if UseDefaultM is false");
static ValueTask DefaultM(T instance) {
Console.WriteLine("Default Behaviour");
return default;
}
}
struct M : IM<int> {}
public static class Program
{
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
static void Main()
{
var m = new M();
if (((IM<int>)m).UseDefaultM)
{
IM<int>.DefaultM(42);
}
else
{
((IM<int>)m).M(42);
}
}
}I was hoping to use this pattern to avoid boxing m, since I would have expected UseDefaultM to be inlined, therefore avoiding boxing m when it's a struct, since either the static DefaultM method will be called, or the instance of IM overrides the DIM for M in which case it doesn't require boxing. However when I look at the jit asm on sharplab, it appears this isn't happening.
EDIT
Note that this does work when IM is non-generic:
using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
interface IM
{
bool UseDefaultM { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => true; }
ValueTask M(int instance) => throw new NotImplementedException("M must be implemented if UseDefaultM is false");
static ValueTask DefaultM(int instance) {
Console.WriteLine("Default Behaviour");
return default;
}
}
struct M : IM {}
public static class Program
{
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
static void Main()
{
var m = new M();
if (((IM)m).UseDefaultM)
{
IM.DefaultM(42);
}
else
{
((IM)m).M(42);
}
}
}The jit works out that IM.DefaultM is always called and removes both UseDefaultM and ((IM)m).M(42) from the generated asm:
Program.Main()
L0000: push ebp
L0001: mov ebp, esp
L0003: sub esp, 0x10
L0006: xor eax, eax
L0008: mov [ebp-8], eax
L000b: mov [ebp-4], eax
L000e: mov [ebp-0x10], eax
L0011: mov [ebp-0xc], eax
L0014: lea ecx, [ebp-0x10]
L0017: mov edx, 0x2a
L001c: call IM.DefaultM(Int32)
L0021: mov esp, ebp
L0023: pop ebp
L0024: retReacted by SingleAccretion
Activity
MichalStrehovsky commentedon Jul 16, 2020
The type of
thiswithin the default interface implementation isIM<int>, notM(it's a reference type, not byref to a valuetype). The code generator would have to retype thethisand then deal with situations where the retyping could be invalid (e.g.bool UseDefaultM(T instance) => new object[] { this }.Length > 0;- placing thethisinto anobject[]is not possible without it being a reference type). The interface being generic doesn't make things easier either.Default interface methods are better to be avoided in high performance code.
YairHalberstadt commentedon Jul 16, 2020
@MichalStrehovsky
It manages to inline it when
IMis not generic:MichalStrehovsky commentedon Jul 16, 2020
#9588 is probably the main reason then.
YairHalberstadt commentedon Jul 16, 2020
A decent workaround is:
AndyAyersMS commentedon Jul 16, 2020
Right, devirtualization is failing in
resolveVirtualMethodHelper.I should plumb through failure reason reporting and pass the reason back to the jit, so it's more obvious on the jit side what is happening. Currently we just see a generic failure:
Going to mark this as future and relabel as a VM issue.
MichalStrehovsky commentedon Jul 17, 2020
The VM side is tracked in #9588 so we can just call this a dup and close.