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

Add special Comparer/EqualityComparer for Nullable<Enum> #68077

Merged
merged 11 commits into from
May 31, 2022

Conversation

EgorBo
Copy link
Member

@EgorBo EgorBo commented Apr 15, 2022

Fixes #67842

Example:

bool Test(DayOfWeek? d1, DayOfWeek? d2) => EqualityComparer<DayOfWeek?>.Default.Equals(d1, d2);

Codegen diff: https://www.diffchecker.com/7qPt1KtF
It doesn't box (allocate) any more. The final codegen is still a bit suboptimal but it's a different problem (Nullable<> is not promoted)

Same codegen for e.g.:

record struct Test(DayOfWeek? Day);

bool Test(Test t1, Test t2) => t1.Equals(t2);

@dotnet-issue-labeler
Copy link

I couldn't figure out the best area label to add to this PR. If you have write-permissions please help me learn by adding exactly one area label.

@ghost ghost assigned EgorBo Apr 15, 2022
@jkotas
Copy link
Member

jkotas commented Apr 15, 2022

Would it be better to instead figure out how to optimize the boxing from the constrained calls to Enum.Equals?

}
return -1;
}
}

// Instantiated internally by VM
internal sealed class NullableEnumEqualityComparer<T> : EqualityComparer<T?> where T : struct, Enum
Copy link
Member

Choose a reason for hiding this comment

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

This needs tests and extra code to ensure that the binary serialization continues to work.

@jkotas
Copy link
Member

jkotas commented Apr 15, 2022

This also needs to be handled in src\coreclr\tools\Common\TypeSystem\IL\Stubs\ComparerIntrinsics.cs

@jkotas
Copy link
Member

jkotas commented Apr 15, 2022

Do we want to add it for Mono too? We generally try to add optimizations like these for all runtimes.

@EgorBo EgorBo force-pushed the add-nullable-enum-equality-comparer branch from 3f14cf4 to 53cbb24 Compare April 16, 2022 18:09
@jkotas
Copy link
Member

jkotas commented Apr 16, 2022

Let's intrinsify Nullable<>.Equals instead

How is this fixing the problem with boxing in EqualityComparer from the linker issue?

@EgorBo
Copy link
Member Author

EgorBo commented Apr 16, 2022

Let's intrinsify Nullable<>.Equals instead

How is this fixing the problem with boxing in EqualityComparer from the linker issue?

@jkotas it's called by the ObjectEqualityComparer.

The current problem that

bool Test(DayOfWeek? d1, DayOfWeek? d2) => EqualityComparer<DayOfWeek?>.Default.Equals(d1, d2);

ends up as Nullable<>.Equals(object) call which is then inlined into Enum.Equals(object) where input enum becomes a multi-use boxing we're not able to handle currently #9118

so while this PR indeed works it's quite verbose and doesn't handle CompareTo so I think it's better to rely on a custom equalitycomparer for now

@jkotas
Copy link
Member

jkotas commented Apr 16, 2022

EqualityComparer<DayOfWeek?>.Default.Equals(d1, d2);
ends up as Nullable<>.Equals(object) call which is then inlined into Enum.Equals(object)

It should end up as NullableEqualityComparer.Equals

NullableEqualityComparer.Equals calls Enum.Equals here:

.

I do not see anything calling Nullable.Equals on this path.

@EgorBo
Copy link
Member Author

EgorBo commented Apr 16, 2022

It should end up as NullableEqualityComparer.Equals

it does not currently because it doesn't pass this check

TypeHandle iequatable = TypeHandle(CoreLibBinder::GetClass(CLASS__IEQUATABLEGENERIC)).Instantiate(nullableInst);
if (nullableInst[0].CanCastTo(iequatable))
{
TypeHandle resultTh = ((TypeHandle)CoreLibBinder::GetClass(CLASS__NULLABLE_COMPARER)).Instantiate(nullableInst);
return CORINFO_CLASS_HANDLE(resultTh.GetMethodTable());
}
🤔 (what if I enable it for enums...)

@jkotas
Copy link
Member

jkotas commented Apr 16, 2022

Ok, make sense.

@EgorBo EgorBo force-pushed the add-nullable-enum-equality-comparer branch from 53cbb24 to dcd2e3a Compare April 17, 2022 10:48
@jkotas
Copy link
Member

jkotas commented Apr 17, 2022

I am wondering whether we can just fix the existing NullableEqualityComparer instead of introducing a new one. Drop IEquatable<T> constraint from NullableEqualityComparer<T> and change x.value.Equals(y.value); in NullableEqualityComparer implementation to EqualityComparer<T>.Default.Equals(x, y)?

@EgorBo
Copy link
Member Author

EgorBo commented Apr 18, 2022

I am wondering whether we can just fix the existing NullableEqualityComparer instead of introducing a new one. Drop IEquatable<T> constraint from NullableEqualityComparer<T> and change x.value.Equals(y.value); in NullableEqualityComparer implementation to EqualityComparer<T>.Default.Equals(x, y)?

Great idea! Let's see how it goes...

@ghost
Copy link

ghost commented Apr 28, 2022

Tagging subscribers to this area: @dotnet/area-system-runtime
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #67842

Example:

bool Test(DayOfWeek? d1, DayOfWeek? d2) => EqualityComparer<DayOfWeek?>.Default.Equals(d1, d2);

Codegen diff: https://www.diffchecker.com/7qPt1KtF
It doesn't box (allocate) any more. The final codegen is still a bit suboptimal but it's a different problem (Nullable<> is not promoted)

Same codegen for e.g.:

record struct Test(DayOfWeek? Day);

bool Test(Test t1, Test t2) => t1.Equals(t2);
Author: EgorBo
Assignees: EgorBo
Labels:

area-System.Runtime

Milestone: -

Copy link
Member

@jkotas jkotas left a comment

Choose a reason for hiding this comment

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

Could you please check asm diffs for the existing NullableEqualityComparer uses to make sure there is no significant code quality regression?

Otherwise looks good.

@EgorBo
Copy link
Member Author

EgorBo commented May 31, 2022

@jkotas Thanks! Sorry for the delay between changes - I was on a month-long "vacation".

The codegen diff is more or less the same:

  1. Nullable Primitives - no diffs
  2. Nullable Struct implementing IEquatable/IComparable - a few redundant movs
  3. Nullable structs and enums - much better codegen and no allocations (boxing)

Had to remove a test in Libraries

not relevant anymore - it used to rely on fact that a round-trip binary serialization should produce the same or assignable object but it doesn't work now.

e.g.

EqualityComparer<T> orig = EqualityComparer<T>.Default;
bf.Serialize(s, orig);
s.Position = 0;
object result = bf.Deserialize(s);
Assert.IsAssignableFrom<EqualityComparer<T>>(result);

For T being Nullable<MyInt16Enum> we used to get ObjectEqualityComparer<Nullable<MyInt16Enum>> object from .Default but now we get NullableEqualityCompdarer<MyInt16Enum>
which after a round-trip serialization becomes ObjectEqualityComparer<MyInt16Enum> due to backward compatibility hack

@EgorBo
Copy link
Member Author

EgorBo commented May 31, 2022

oh, wait a minute that might be a bug actually - let me check

@EgorBo EgorBo force-pushed the add-nullable-enum-equality-comparer branch from 5be10dc to 481e782 Compare May 31, 2022 14:29
@EgorBo
Copy link
Member Author

EgorBo commented May 31, 2022

Right, it was my bug, fixed with 481e782

@EgorBo
Copy link
Member Author

EgorBo commented May 31, 2022

Failure is unrelated (#68511)

@EgorBo EgorBo merged commit 8a043bf into dotnet:main May 31, 2022
@ghost ghost locked as resolved and limited conversation to collaborators Jul 1, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

EqualityComparer<TEnum?>.Default allocates
3 participants