Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unify Default Comparer logic #88006

Merged
merged 46 commits into from
Jul 5, 2023
Merged

Unify Default Comparer logic #88006

merged 46 commits into from
Jul 5, 2023

Conversation

MichalPetryka
Copy link
Contributor

@MichalPetryka MichalPetryka commented Jun 24, 2023

Unifies (Equality)Comparer.Default logic so that it matches between CoreCLR, NativeAOT, Mono in runtime and intrinsic implementations.

Fixes devirt behaviour around Nullable types.

Also enables devirt for non final types in CoreCLR and NativeAOT.

Required for #87579.
Fixes #87391.

@dotnet-issue-labeler dotnet-issue-labeler bot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jun 24, 2023
@ghost ghost added the community-contribution Indicates that the PR has been added by a community member label Jun 24, 2023
@ghost
Copy link

ghost commented Jun 24, 2023

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

Issue Details

Unifies (Equality)Comparer.Default logic so that it matches between CoreCLR, NativeAOT, Mono in runtime and intrinsic implementations.

Also enables devirt for non final types in CoreCLR and NativeAOT.

Required for #87579.

Author: MichalPetryka
Assignees: -
Labels:

area-CodeGen-coreclr, community-contribution

Milestone: -

@EgorBo
Copy link
Member

EgorBo commented Jun 24, 2023

It should fix #87391

@MichalPetryka MichalPetryka marked this pull request as ready for review June 25, 2023 12:11
@MichalPetryka
Copy link
Contributor Author

@jkotas Roslyn seems to be emitting some invalid IL for the enum tests I've added, going to report it. Should I remove them for now?

@MichalPetryka
Copy link
Contributor Author

What is the type that it does not work for?

Some examples:


This one seems okay?
EqualityComparer<TKey1>.Default.Equals(_pKey1, other._pKey1)

This one doesn't however, since we don't have a type here since the type is shared:

; Assembly listing for method Microsoft.CSharp.RuntimeBinder.Semantics.TypeTable+KeyPair`2[System.__Canon,System.Nullable`1[int]]:Equals(System.Object):bool:this (FullOpts)

@jkotas
Copy link
Member

jkotas commented Jul 1, 2023

I think you want to reorder the checks in getDefaultEqualityComparerClassHelper like this:

if (Nullable::IsNullableType(elemTypeHnd))
{
   ...CLASS__NULLABLE_EQUALITYCOMPARER...
}

if (elemTypeHnd.IsEnum())
{
   ...CLASS__ENUM_EQUALITYCOMPARER...
}

if (elemTypeHnd.IsCanonicalSubtype())
{
   Bail for shared generic types for now. We can improve this case later.
   return NULL;
}

if (elemTypeHnd.CanCastTo(...))
{
   ...CLASS__GENERIC_EQUALITYCOMPARER...
}

return ...CLASS__OBJECT_EQUALITYCOMPARER...;

@jkotas
Copy link
Member

jkotas commented Jul 2, 2023

Would it make sense to split the JIT change into separate PR? The Mono, VM and BCL changes in this PR are good and they can go in. The JIT change may need some more work, and it may be a good idea to do it separately so that it is easier to look at its impact from jit-diffs.

@MichalPetryka
Copy link
Contributor Author

@MihuBot

TypeHandle resultTh = ((TypeHandle)targetClass->GetCanonicalMethodTable()).Instantiate(inst);
return CORINFO_CLASS_HANDLE(resultTh.GetMethodTable());
}
if (elemTypeHnd.IsCanonicalSubtype())
Copy link
Member

@jkotas jkotas Jul 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CanCastTo(IEquatable) case needs to be after this check, like I have suggested in my comment.

Or the IsCanonicalSubtype check needs to be the very first thing in the method - we would give up on more cases that way. (It would mirror what happens today.)

Or the CanCastTo(IEquatable) check needs to be implemented against generic type definition for the IsCanonicalSubtype case. This would be the most precise option, but there would be still some ambiguous cases where we would have to give up.

Here is example of a bug that would be hit by the code as written:

using System;
using System.Threading;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;

for (int i = 0; i < 100; i++)
{
   Internal.Console.WriteLine(Test<string, object>().ToString());
   Thread.Sleep(1);
}

[MethodImpl(MethodImplOptions.NoInlining)]
static bool Test<T,U>()
{
    return EqualityComparer<G<T,U>>.Default.Equals(default, default);
}

struct G<T,U> : IEquatable<G<U,T>>
{
   public bool Equals(G<U,T> x) => false;
}

It will start printing true correctly, and then flips to printing false as the incorrect Tier1 optimization kicks in.

Copy link
Contributor Author

@MichalPetryka MichalPetryka Jul 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(It would mirror what happens today.)

It wouldn't, it'd then block Struct<_Canon> too, which you were concerned about before.
The issue right now is that this seems to still be devirtualizing __Canon as ObjectEqualityComparer for some reason (see those diffs). I have no idea what's going on here, could WellKnownArg be returning some weird handle here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, it won't mirror the bugs. Notice that the bug demonstrated by my G<T,U> repro has been shipping for a while (just tried it on .NET 7).

Struct<_Canon> needs the third option (use generic type definition to check for IEquatable implementation). Otherwise, it is better to block it rather than to have functional bugs that manifest as observable behavior differences between Tier0 and Tier1 JIT.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see those diffs

This links to my comment. Did you meant to link to some diffs somewhere?

Copy link
Contributor Author

@MichalPetryka MichalPetryka Jul 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This links to my comment. Did you meant to link to some diffs somewhere?

Yeah, seems like Copy link on that GitHub message didn't work so I still had a link to your comment in my clipboard instead. Fixed that link.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There diffs look odd. Are you able to reproduce the behavior locally? It can be that the bot picked up stale bits from somewhere.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new diffs look good to me, but I do not think the difference can be explained by the one commit that you have pushed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new diffs look good to me, but I do not think the difference can be explained by the one commit that you have pushed.

The bot seems to be having some issues with picking up VM changes, after a fix CoreLib changes look good but frameworks ones still have the weird diff, not sure if it's still an issue with the bot.

@MichalPetryka
Copy link
Contributor Author

@MihuBot

@jkotas
Copy link
Member

jkotas commented Jul 5, 2023

  • Could you please add a test case for the repro from Unify Default Comparer logic #88006 (comment)

  • I have implemented correct handling of shared types in jkotas@527e8c7 . Feel free to cherry-pick the commit into this PR if it looks good to you. It helps with devirtualization, but it does help with inlining. Inlining typically runs into the current shared generic code inlining limitations.

The change looks good to me otherwise. Thank you!

@jkotas
Copy link
Member

jkotas commented Jul 5, 2023

Could you please add a test case for the repro from #88006 (comment)

It should be one more case in src/tests/JIT/opt/Devirtualization/Comparer_get_Default.cs like the cases that are there already. The JIT tests are run both with optimizations on and off, so we would find the bug that way.

@MichalPetryka
Copy link
Contributor Author

  • I have implemented correct handling of shared types in jkotas@527e8c7 . Feel free to cherry-pick the commit into this PR if it looks good to you. It helps with devirtualization, but it does help with inlining. Inlining typically runs into the current shared generic code inlining limitations.

I'd prefer that to be in a separate PR after this gets merged to keep diffs separate and avoid any potential issues.

@jkotas jkotas merged commit 18321c6 into dotnet:main Jul 5, 2023
@jkotas
Copy link
Member

jkotas commented Jul 5, 2023

  • I have implemented correct handling of shared types in jkotas@527e8c7 . Feel free to cherry-pick the commit into this PR if it looks good to you. It helps with devirtualization, but it does help with inlining. Inlining typically runs into the current shared generic code inlining limitations.

I'd prefer that to be in a separate PR after this gets merged to keep diffs separate and avoid any potential issues.

I am going to leave this for .NET 9.

@stephentoub
Copy link
Member

stephentoub commented Jul 6, 2023

I'm not sure why tests didn't fail here, but this appears to have broken some System.Runtime.Serialization.Formatters tests:
#88453

  Discovering: System.Runtime.Serialization.Formatters.Tests (method display = ClassAndMethod, method display options = None)
  Discovered:  System.Runtime.Serialization.Formatters.Tests (found 79 of 80 test cases)
  Starting:    System.Runtime.Serialization.Formatters.Tests (parallel test collections = on, max threads = 2)
    System.Runtime.Serialization.Formatters.Tests.DisableBitTests.DisabledAlwaysInBrowser [SKIP]
      Condition(s) not met: "IsBinaryFormatterSuppressedOnThisPlatform"
    System.Runtime.Serialization.Formatters.Tests.BinaryFormatterTests.ValidateAgainstBlobs(obj: GenericEqualityComparer`1 { }, blobs: [System.Runtime.Serialization.Formatters.Tests.TypeSerializableValue, System.Runtime.Serialization.Formatters.Tests.TypeSerializableValue]) [FAIL]
      The stored blob for type System.Collections.Generic.GenericEqualityComparer`1[[System.Byte, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]] is outdated and needs to be updated.
      
      -------------------- Stored blob ---------------------
      Encoded: AAEAAAD/////AQAAAAAAAAAEAQAAAC9TeXN0ZW0uQ29sbGVjdGlvbnMuR2VuZXJpYy5CeXRlRXF1YWxpdHlDb21wYXJlcgAAAAAL
      Decoded: �����????�������������/System.Collections.Generic.ByteEqualityComparer�����
      
      --------------- Runtime generated blob ---------------
      Encoded: AAEAAAD/////AQAAAAAAAAAEAQAAAJABU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuR2VuZXJpY0VxdWFsaXR5Q29tcGFyZXJgMVtbU3lzdGVtLkJ5dGUsIG1zY29ybGliLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV1dAAAAAAs=
      Decoded: �����????�������������?�System.Collections.Generic.GenericEqualityComparer`1[[System.Byte, mscorlib, Version=0.0.0.0 Culture=neutral, PublicKeyToken=b77a5c561934e089]]�����
      Expected: True
      Actual:   False
      Stack Trace:
        /_/src/libraries/System.Runtime.Serialization.Formatters/tests/BinaryFormatterTests.cs(615,0): at System.Runtime.Serialization.Formatters.Tests.BinaryFormatterTests.SanityCheckBlob(Object obj, TypeSerializableValue[] blobs)
        /_/src/libraries/System.Runtime.Serialization.Formatters/tests/BinaryFormatterTests.cs(111,0): at System.Runtime.Serialization.Formatters.Tests.BinaryFormatterTests.ValidateAndRoundtrip(Object obj, TypeSerializableValue[] blobs, Boolean isEqualityComparer)
        /_/src/libraries/System.Runtime.Serialization.Formatters/tests/BinaryFormatterTests.cs(73,0): at System.Runtime.Serialization.Formatters.Tests.BinaryFormatterTests.ValidateAgainstBlobs(Object obj, TypeSerializableValue[] blobs)
           at InvokeStub_BinaryFormatterTests.ValidateAgainstBlobs(Object, Object, IntPtr*)
           at System.Reflection.MethodInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr)

@EgorBo
Copy link
Member

EgorBo commented Jul 6, 2023

Presumably GenericEqualityComparer should have a backward compatibility patch for binary formatter, something like this:

public void GetObjectData(SerializationInfo info, StreamingContext context)
{
// For back-compat we need to serialize the comparers for enums with underlying types other than int as ObjectEqualityComparer
if (Type.GetTypeCode(typeof(T)) != TypeCode.Int32)
{
info.SetType(typeof(ObjectEqualityComparer<T>));
}
}

for T being byte it should report ByteEqualityComparer

@MichalPetryka

@MichalPetryka
Copy link
Contributor Author

for T being byte it should report ByteEqualityComparer

@jkotas do we want that or maybe changing the tests to expect GenericEqualityComparer would be preferred here?

@stephentoub
Copy link
Member

I'm not particularly concerned about someone serializing the comparer on .NET 8 and when deserialized on .NET Framework having it be a different kind of comparer than it would otherwise have been, especially as BinaryFormatter is on its way out. @GrabYourPitchforks, do we need to fix anything here?

@jkotas
Copy link
Member

jkotas commented Jul 6, 2023

Binary serialization of compares does not guarantee that you get the fastest possible comparer after deserialization. It only guarantees that you get a functionally equivalent comparer.

GenericEqualityComparer is going to work fine for bytes on .NET Framework. We do not need to do anything special for it.

It is not the case for EnumEqualityComparer. EnumEqualityComparer only works for int32 underlying type on .NET Framework, it does not work for other underlying types. It is why the code is going what it is doing.

@MichalPetryka
Copy link
Contributor Author

I'm not sure why tests didn't fail here

Did anybody figure this out btw?

@stephentoub
Copy link
Member

I'm not sure why tests didn't fail here

Did anybody figure this out btw?

The relevant test is part of the libraries tests, and I believe this PR ended up triggering legs for the libraries tests that ran either on mono or on a checked coreclr, and the particular test suite is disabled for checked coreclr because it's too slow.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-VM-coreclr community-contribution Indicates that the PR has been added by a community member
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Test failure JIT/opt/Devirtualization/Comparer_get_Default/Comparer_get_Default.dll
8 participants