dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.26k stars 4.73k forks source link

Devirtualization isn't acting on devirtualizable calls past the first #39519

Open NinoFloris opened 4 years ago

NinoFloris commented 4 years ago

Ran from a VM due to https://github.com/dotnet/BenchmarkDotNet/issues/1499


BenchmarkDotNet=v0.12.1, OS=Windows 10.0.17763.1039 (1809/October2018Update/Redstone5)
Intel Core i7-4980HQ CPU 2.80GHz (Haswell), 1 CPU, 4 logical and 4 physical cores
.NET Core SDK=5.0.100-preview.6.20318.15
  [Host]   : .NET Core 5.0.0 (CoreCLR 5.0.20.30506, CoreFX 5.0.20.30506), X64 RyuJIT
  ShortRun : .NET Core 5.0.0 (CoreCLR 5.0.20.30506, CoreFX 5.0.20.30506), X64 RyuJIT

Job=ShortRun  IterationCount=3  LaunchCount=1  
WarmupCount=3  
Method Mean Error StdDev Code Size
Combined 3.6617 ns 0.9868 ns 0.0541 ns 42 B
Separate 3.6139 ns 0.4095 ns 0.0224 ns 42 B
JustMe 0.0000 ns 0.0000 ns 0.0000 ns 6 B

The idea is to do a two step virtual call, once dispatching from this (Combined) and once to a separate sealed class (Separate), JustMe is the control as it devirtualizes correctly.

using System;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running;

namespace test2
{
    public interface IMe 
    {
        bool Hello();   
    }

    public interface IYou 
    {
        IMe You { get; }   
    }

    public sealed class Combined : IMe, IYou
    {
        public IMe You => this;
        public bool Hello() => true;
    }

    public sealed class MeImpl : IMe
    {
        public bool Hello() => true;
    }

    public sealed class YouImpl : IYou
    {
        public IMe You => new MeImpl();
    }

    [ShortRunJob, DisassemblyDiagnoser(exportGithubMarkdown: true)]
    public class Program
    {
        [Benchmark]
        public bool Combined() => new Combined().You.Hello();

        [Benchmark]
        public bool Separate() => new YouImpl().You.Hello();

        [Benchmark]
        public bool JustMe() => new MeImpl().Hello();

        static void Main(string[] args)
        {
            BenchmarkRunner.Run<Program>();
        }
    }
}

And the resulting asm:

## .NET Core 5.0.0 (CoreCLR 5.0.20.30506, CoreFX 5.0.20.30506), X64 RyuJIT ```assembly ; test2.Program.Combined() sub rsp,28 mov rcx,offset MT_test2.Combined call CORINFO_HELP_NEWSFAST mov rcx,rax mov rax,[7FFA0807B738] add rsp,28 jmp rax ; Total bytes of code 36 ``` ```assembly ; test2.Combined.Hello() mov eax,1 ret ; Total bytes of code 6 ``` ## .NET Core 5.0.0 (CoreCLR 5.0.20.30506, CoreFX 5.0.20.30506), X64 RyuJIT ```assembly ; test2.Program.Separate() sub rsp,28 mov rcx,offset MT_test2.MeImpl call CORINFO_HELP_NEWSFAST mov rcx,rax mov rax,[7FFA0808B7F0] add rsp,28 jmp rax ; Total bytes of code 36 ``` ```assembly ; test2.MeImpl.Hello() mov eax,1 ret ; Total bytes of code 6 ``` ## .NET Core 5.0.0 (CoreCLR 5.0.20.30506, CoreFX 5.0.20.30506), X64 RyuJIT ```assembly ; test2.Program.JustMe() mov eax,1 ret ; Total bytes of code 6 ```

Expectation would be that all benchmarks compile down to mov eax, 1

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

AndyAyersMS commented 4 years ago

It's not readily apparent from the disassembly above, but both calls are in fact devirtualized. But this happens "late" as the object being dispatched is the return value from a call, and because of this, we currently can't do inlining or box removal optimizations.

;; early attempt during importation

impDevirtualizeCall: Trying to devirtualize interface call:
    class for 'this' is IMe (attrib 00200400)
    base method is IMe::Hello
No unique implementor of interface 00000000D1FFAB1E (IMe), sorry

;; late attempt after inlining

impDevirtualizeCall: Trying to devirtualize interface call:
    class for 'this' is Combined [exact] (attrib 20000010)
    base method is IMe::Hello
--- base class is interface
    devirt to Combined::Hello -- exact
               [000009] --C-G-------              *  CALLV stub int    IMe.Hello
               [000006] ------------ this in rcx  \--*  LCL_VAR   ref    V02 tmp1         
    exact; can devirtualize
... after devirt...
               [000009] --C-G-------              *  CALL nullcheck int    Combined.Hello
               [000006] ------------ this in rcx  \--*  LCL_VAR   ref    V02 tmp1         

The calls end up getting invoked via an indirection cell which makes them look like they're possibly still virtual.

AndyAyersMS commented 4 years ago

Linked this into the devirtualization "todo" issue #7541. Marking as future.