Skip to content

Not inlining trivial default interface method on known struct #39419

@YairHalberstadt

Description

@YairHalberstadt

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: ret

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions