Skip to content

Generic virtual method devirtualization does not happen #32129

@NinoFloris

Description

@NinoFloris

Consider the following benchmark example:

    public interface IFoo 
    {
        object Foo();
    }

    public interface IFooGeneric 
    {
        T FooGeneric<T>();
    }

    public sealed class Impl: IFoo, IFooGeneric
    {
        [MethodImpl(MethodImplOptions.NoInlining)]
        public object Foo() => null;

        [MethodImpl(MethodImplOptions.NoInlining)]
        public T FooGeneric<T>() => default;
    }

    public class DevirtBenchmark
    {
        const int Operations = 100000;
        static Impl ImplInstance;
        static IFoo IFooInstance;
        static IFooGeneric IFooGenericInstance;

        [GlobalSetup(Target = "ImplFooGeneric,IFooViaImpl,IFooGenericViaImpl")]
        public void SetupImpl() => ImplInstance = new Impl();

        [GlobalSetup(Target = "IFoo")]
        public void SetupIFoo() => IFooInstance = new Impl();

        [GlobalSetup(Target = "IFooGeneric")]
        public void SetupIFooGenric() => IFooGenericInstance = new Impl();

        [Benchmark(OperationsPerInvoke = Operations)]
        public void ImplFooGeneric()
        {
            for (int i = 0; i < Operations; i++)
            {
                ImplInstance.FooGeneric<object>();
            }
        }

        [Benchmark(OperationsPerInvoke = Operations)]
        public void IFoo()
        {
            for (int i = 0; i < Operations; i++)
            {
                IFooInstance.Foo();
            }
        }

        [Benchmark(OperationsPerInvoke = Operations)]
        public void IFooViaImpl()
        {
            for (int i = 0; i < Operations; i++)
            {
                ((IFoo)ImplInstance).Foo();
            }
        }

        [Benchmark(OperationsPerInvoke = Operations)]
        public void IFooGeneric()
        {
            for (int i = 0; i < Operations; i++)
            {
                IFooGenericInstance.FooGeneric<object>();
            }
        }

        [Benchmark(OperationsPerInvoke = Operations)]
        public void IFooGenericViaImpl()
        {
            for (int i = 0; i < Operations; i++)
            {
                ((IFooGeneric)ImplInstance).FooGeneric<object>();
            }
        }
    }

Results show the JIT can succesfully devirtualize IFooViaImpl by observing the concrete type before the cast. Yet it fails to do so for IFooGenericViaImpl while those - extremely slow calls - would benefit most from this working correctly.

// * Summary *

BenchmarkDotNet=v0.12.0, OS=macOS Mojave 10.14.6 (18G3020) [Darwin 18.7.0]
Intel Core i7-4980HQ CPU 2.80GHz (Haswell), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.101
  [Host]     : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT DEBUG
  DefaultJob : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT


|             Method |     Mean |     Error |    StdDev |
|------------------- |---------:|----------:|----------:|
|     ImplFooGeneric | 1.818 ns | 0.0081 ns | 0.0072 ns |
|               IFoo | 2.360 ns | 0.0354 ns | 0.0331 ns |
|        IFooViaImpl | 1.546 ns | 0.0056 ns | 0.0052 ns |
|        IFooGeneric | 7.758 ns | 0.0605 ns | 0.0566 ns |
| IFooGenericViaImpl | 7.739 ns | 0.0242 ns | 0.0227 ns |

/cc @AndyAyersMS

EDIT: I have added a direct generic call for scale
What I also noticed is that the class itself needs to be sealed as well for IFooViaImpl to devirtualize, yet that method is already final in IL, this seems like a rather small fix.

category:cq
theme:devirtualization
skill-level:expert
cost:large

Metadata

Metadata

Assignees

No one assigned

    Labels

    JitUntriagedCLR JIT issues needing additional triagearea-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions