dotnet / runtime

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

JIT cannot devirtualize interface call in trivial cases #63283

Closed sakno closed 2 years ago

sakno commented 2 years ago

Description

JIT cannot devirtualize interface call in very trivial cases.

Source code:

using System;
using System.Runtime.CompilerServices;

public static class C {
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static T As<T>(T obj) where T : class => obj;

    public static int Cmp(String str) => As<IComparable<string>>(str).CompareTo(string.Empty);

    public static int Cmp2(String str) => str.CompareTo(string.Empty);

    public static int Cmp3(String str) => ((IComparable<string>)str).CompareTo(string.Empty);

    public static int Cmp4(IComparable<string> str) => str.CompareTo(string.Empty);
}

Output assembly:

C.Cmp(System.String)
    L0000: push ebp
    L0001: mov ebp, esp
    L0003: mov edx, [0x8d72018]
    L0009: call dword ptr [0x11317000]
    L000f: pop ebp
    L0010: ret

C.Cmp2(System.String)
    L0000: mov edx, [0x8d72018]
    L0006: cmp [ecx], ecx
    L0008: push 0
    L000a: call System.String.Compare(System.String, System.String, System.StringComparison)
    L000f: ret

C.Cmp3(System.String)
    L0000: push ebp
    L0001: mov ebp, esp
    L0003: mov edx, [0x8d72018]
    L0009: call dword ptr [0x11317004]
    L000f: pop ebp
    L0010: ret

C.Cmp4(System.IComparable`1<System.String>)
    L0000: push ebp
    L0001: mov ebp, esp
    L0003: mov edx, [0x8d72018]
    L0009: call dword ptr [0x11317008]
    L000f: pop ebp
    L0010: ret

Cmp2 is correctly devirtualized and inlined. I'm expecting that the assembly code should be the same for all cases except Cmp4.

Configuration

.NET 6.0.101 Ubuntu 20.04.3 5.4.0-91-generic x86_64

Regression?

Probably yes, but I didn't check.

ghost commented 2 years ago

Tagging subscribers to this area: @JulieLeeMSFT See info in area-owners.md if you want to be subscribed.

Issue Details
### Description JIT cannot devirtualize interface call in very trivial cases. Source code: ```csharp using System; using System.Runtime.CompilerServices; public static class C { [MethodImpl(MethodImplOptions.AggressiveInlining)] private static T As(T obj) where T : class => obj; public static int Cmp(String str) => As>(str).CompareTo(string.Empty); public static int Cmp2(String str) => str.CompareTo(string.Empty); public static int Cmp3(String str) => ((IComparable)str).CompareTo(string.Empty); public static int Cmp4(IComparable str) => str.CompareTo(string.Empty); } ``` Output assembly: ```asm C.Cmp(System.String) L0000: push ebp L0001: mov ebp, esp L0003: mov edx, [0x8d72018] L0009: call dword ptr [0x11317000] L000f: pop ebp L0010: ret C.Cmp2(System.String) L0000: mov edx, [0x8d72018] L0006: cmp [ecx], ecx L0008: push 0 L000a: call System.String.Compare(System.String, System.String, System.StringComparison) L000f: ret C.Cmp3(System.String) L0000: push ebp L0001: mov ebp, esp L0003: mov edx, [0x8d72018] L0009: call dword ptr [0x11317004] L000f: pop ebp L0010: ret C.Cmp4(System.IComparable`1) L0000: push ebp L0001: mov ebp, esp L0003: mov edx, [0x8d72018] L0009: call dword ptr [0x11317008] L000f: pop ebp L0010: ret ``` `Cmp2` is correctly devirtualized. I'm expecting that the assembly code should be the same for all cases except `Cmp4`. ### Configuration .NET 6.0.101 Ubuntu 20.04.3 5.4.0-91-generic x86_64 ### Regression? Probably yes, but I didn't check.
Author: sakno
Assignees: -
Labels: `tenet-performance`, `area-CodeGen-coreclr`, `untriaged`
Milestone: -
EgorBo commented 2 years ago

Cmp2 and Cmp3 have the same codegen in .NET 7.0 Cmp1 is a known JIT's limitation - we don't re-visit calls we marked as non-devirtualizable/non-inlinineable candidates after we inlined some inner calls in the same method.

But it does happen when Dynamic PGO is turned on.

sakno commented 2 years ago

Any chance to backport JIT behavior as you described to .NET 6 ?

EgorBo commented 2 years ago

Any chance to backport JIT behavior as you described to .NET 6 ?

Dynamic PGO? you can enable it in .NET 6.0, see https://gist.github.com/EgorBo/dc181796683da3d905a5295bfd3dd95b#how-can-i-try-it-on-my-production

sakno commented 2 years ago

No, not Dynamic PGO. I'm talking about to have the same codegen for Cmp2 and Cmp3 in .NET 6.

EgorBo commented 2 years ago

No, not Dynamic PGO. I'm talking about to have the same codegen for Cmp2 and Cmp3 in .NET 6.

Unlikely, the bar to backport changes to .NET 6.0 is very high to include an optimization.

AndyAyersMS commented 2 years ago

I think we should be able to get the Cmp case via late devirtualization, but for some reason the interface class we end up with doesn't seem to be compatible with string and so we fail to do so now. Need to analyze further.

*** CMP

impDevirtualizeCall: Trying to devirtualize interface call: (late)

    class for 'this' is System.String [final] (attrib 20040010)
    base method is System.IComparable`1[__Canon]::CompareTo
--- base class is interface
--- no derived method: object class could not be cast to interface class
    Class not final or exact
No guarded devirt during late devirtualization

*** CMP3

impDevirtualizeCall: Trying to devirtualize interface call: (early)
    class for 'this' is System.String [final] (attrib 20040010)
    base method is System.IComparable`1[__Canon]::CompareTo
--- base class is interface
    devirt to System.String::CompareTo -- final class
AndyAyersMS commented 2 years ago

Seems like the issue is that in the Cmp case we end up losing the generic context for the As (which should be somehow associated with the return value) and so are trying to cast from System.String to IComparable<_Canon> sans context, and this fails in the runtime, whereas with Cmp3 we have appropriate context and are casting to IComparable<System.String>.

When we propagate a return value out of a generic method to the non-generic root, we ideally should be able to determine the proper unshared type for that value. This might work roughly as follows:

AndyAyersMS commented 2 years ago

Another perhaps more robust option would be to retype the return value, if it's a ref type, and we have a context.

AndyAyersMS commented 2 years ago

Fix for this in PR #63420.

sakno commented 2 years ago

Closing issue as PR has been merged.