-
Notifications
You must be signed in to change notification settings - Fork 2.7k
JIT: devirtualization support for EqualityComparer<T>.Default #14125
JIT: devirtualization support for EqualityComparer<T>.Default #14125
Conversation
Still some work to do to hook this up to SPMI, bump the jit guid, update desktop, etc, but want to settle on the shape and content of the jit interface changes first and get a little testing underway. @davidwrighton @jkotas PTAL |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The JIT-EE interface change looks good to me.
src/vm/jitinterface.cpp
Outdated
{ | ||
case ELEMENT_TYPE_I1: | ||
{ | ||
targetClass = MscorlibBinder::GetClass(CLASS__SHORT_ENUM_EQUALITYCOMPARER); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copy&paste mistake - SHORT_ENUM_EQUALITYCOMPARER
should be for I2
.
Sample codegen inspired by this test method: class T
{
enum E { ... }
public static bool Compare(ref ValueTuple<byte, E, int> a, ref ValueTuple<byte, E, int> b)
{
return a.Equals(b);
}
} Note that the calls to Also wonder if there's anything clever we can do about the class init calls here... Devirtualized virtual call to System.Collections.Generic.EqualityComparer`1[Byte]:Equals; now direct call to System.Collections.Generic.ByteEqualityComparer:Equals [exact]
Devirtualized virtual call to System.Collections.Generic.EqualityComparer`1[E]:Equals; now direct call to System.Collections.Generic.EnumEqualityComparer`1[E][T+E]:Equals [exact]
Devirtualized virtual call to System.Collections.Generic.EqualityComparer`1[Int32]:Equals; now direct call to System.Collections.Generic.GenericEqualityComparer`1[Int32][System.Int32]:Equals [exact]
Inlines into 06001786 System.ValueTuple`3[Byte,E,Int32][System.Byte,T+E,System.Int32]:Equals(struct):bool:this
[1 IL=0000 TR=000001 060039A1] [below ALWAYS_INLINE size] System.Collections.Generic.EqualityComparer`1[Byte][System.Byte]:get_Default():ref
[2 IL=0017 TR=000009 060039C2] [below ALWAYS_INLINE size] System.Collections.Generic.ByteEqualityComparer:Equals(ubyte,ubyte):bool:this
[3 IL=0024 TR=000023 060039A1] [below ALWAYS_INLINE size] System.Collections.Generic.EqualityComparer`1[E][T+E]:get_Default():ref
[0 IL=0041 TR=000031 060039C9] [FAILED: unprofitable inline] System.Collections.Generic.EnumEqualityComparer`1[E][T+E]:Equals(int,int):bool:this
[4 IL=0048 TR=000041 060039A1] [below ALWAYS_INLINE size] System.Collections.Generic.EqualityComparer`1[Int32][System.Int32]:get_Default():ref
[0 IL=0065 TR=000049 060039AA] [FAILED: noinline per IL/cached result] System.Collections.Generic.GenericEqualityComparer`1[Int32][System.Int32]:Equals(int,int):bool:this
; Assembly listing for method System.ValueTuple`3[Byte,E,Int32][System.Byte,T+E,System.Int32]:Equals(struct):bool:this
; Emitting BLENDED_CODE for X64 CPU with AVX
; optimized code
; rsp based frame
; partially interruptible
; Final local variable assignments
;
; V00 this [V00,T01] ( 5, 4 ) byref -> rdi this
; V01 arg1 [V01,T00] ( 5, 8 ) byref -> rsi
; V02 tmp0 [V02,T02] ( 2, 4 ) ubyte -> rcx
; V03 tmp1 [V03,T03] ( 2, 4 ) ubyte -> rdx
;* V04 tmp2 [V04 ] ( 0, 0 ) int -> zero-ref V07.Item2(offs=0x00) P-INDEP
;* V05 tmp3 [V05 ] ( 0, 0 ) int -> zero-ref V07.Item3(offs=0x04) P-INDEP
;* V06 tmp4 [V06 ] ( 0, 0 ) ubyte -> zero-ref V07.Item1(offs=0x08) P-INDEP
;* V07 tmp5 [V07 ] ( 0, 0 ) struct (16) zero-ref
; V08 tmp6 [V08,T04] ( 2, 2 ) ref -> rcx
; V09 tmp7 [V09,T05] ( 2, 2 ) ref -> rcx
; V10 OutArgs [V10 ] ( 1, 1 ) lclBlk (32) [rsp+0x00]
;
; Lcl frame size = 40
G_M31421_IG01:
57 push rdi
56 push rsi
4883EC28 sub rsp, 40
488BF9 mov rdi, rcx
488BF2 mov rsi, rdx
G_M31421_IG02:
48B928305B41FF7F0000 mov rcx, 0x7FFF415B3028
BA01000000 mov edx, 1
E80081375F call CORINFO_HELP_CLASSINIT_SHARED_DYNAMICCLASS
0FB64F08 movzx rcx, byte ptr [rdi+8]
0FB65608 movzx rdx, byte ptr [rsi+8]
3BCA cmp ecx, edx
7567 jne SHORT G_M31421_IG04
48B9F8555B41FF7F0000 mov rcx, 0x7FFF415B55F8
33D2 xor edx, edx
E8E380375F call CORINFO_HELP_CLASSINIT_SHARED_DYNAMICCLASS
48B928288C6807020000 mov rcx, 0x207688C2828
488B09 mov rcx, gword ptr [rcx]
8B17 mov edx, dword ptr [rdi]
448B06 mov r8d, dword ptr [rsi]
E8A4FCFFFF call System.Collections.Generic.EnumEqualityComparer`1[E][T+E]:Equals(int,int):bool:this
85C0 test eax, eax
743B je SHORT G_M31421_IG04
48B928305B41FF7F0000 mov rcx, 0x7FFF415B3028
BA4F000000 mov edx, 79
E8B480375F call CORINFO_HELP_CLASSINIT_SHARED_DYNAMICCLASS
48B930288C6807020000 mov rcx, 0x207688C2830
488B09 mov rcx, gword ptr [rcx]
8B5704 mov edx, dword ptr [rdi+4]
448B4604 mov r8d, dword ptr [rsi+4]
48B8406340A0FF7F0000 mov rax, 0x7FFFA0406340
G_M31421_IG03:
4883C428 add rsp, 40
5E pop rsi
5F pop rdi
48FFE0 rex.jmp rax
G_M31421_IG04:
33C0 xor eax, eax
G_M31421_IG05:
4883C428 add rsp, 40
5E pop rsi
5F pop rdi
C3 ret
; Total bytes of code 156, prolog size 6 for method System.ValueTuple`3[Byte,E,Int32][System.Byte,T+E,System.Int32]:Equals(struct):bool:this |
@AndyAyersMS, just as an FYI. I have a PR which is adding new named intrinsics as well: #14119 One of us will probably end up needing to change our enum values, depending on who gets merged first |
Yep, was just looking at your change. Likely your change will merge first. I need to make anticipatory changes over in desktop before mine goes into CoreCLR (the jit source is mirrored but the jit interface sources are not) and also work on all the various SPMI implementations. |
Does it treat IEquatable structs and classes differently? i.e. class would need to be |
Added change for the intrinsic to the Dict change have been working on d0c3b35 |
Note that the scope of devirtualization here is very narrow -- the comparer call must literally be directly made off of the value produced by The dynamic class init call / static base fetch included in For instance, it not going to be easy to fix code like I did some ad-hoc alteration to the jit to suppress the class init check in this case since at least a cursory scan of the comparer methods returned by |
Are you be happy with doing this, or do you think it would be better to treat the whole |
It is fairly surgical, so yeah, I think perhaps we could live with it. The jit would simply delete the code produced by Doing that that plus adding some aggressive inlining attributes on the comparer methods (since they are much simpler than they appear from an IL scan) and we'd be in a pretty good place wherever We'd still need to duplicate code in Since the jit knows the exact type of the default comparer, if devirtualization fails at some call site where the comparer type is if (typeof(comparer) == typeof(EqualityComparer<T>.Default)) // hopefully just *this == constant
comparer.SpecificComparer<T>.Equals(x,y); // devirtualized, possibly inlined
else
comparer.Equals(x,y); // fallback interface call for custom comparer cases If we do this, we'd possibly get some improvements without touching the collection source code. |
Last commit is probably too aggressive with An alternative is to clear out the side effects of the |
3c792c1
to
d547b8a
Compare
Rebased on top of changes from #14119. Filled in SPMI parts and changed the jit GUID. Changes are getting closer but not quite ready. Still need to look at a few things:
|
Ok, code is probably in its final form. ValueTuple example from above now gives: 0FB64108 movzx rax, byte ptr [rcx+8]
440FB64208 movzx r8, byte ptr [rdx+8]
413BC0 cmp eax, r8d
7519 jne SHORT G_M4626_IG04
8B01 mov eax, dword ptr [rcx]
448B02 mov r8d, dword ptr [rdx]
413BC0 cmp eax, r8d
750F jne SHORT G_M4626_IG04
8B4104 mov eax, dword ptr [rcx+4]
8B5204 mov edx, dword ptr [rdx+4]
3BC2 cmp eax, edx
0F94C0 sete al
0FB6C0 movzx rax, al
G_M4626_IG03:
C3 ret
G_M4626_IG04:
33C0 xor eax, eax
G_M4626_IG05:
C3 ret |
@dotnet/jit-contrib anyone want to take a look at the jit changes? |
@JosephTremoulet maybe I can bug you to review this? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I only reviewed the JIT files. Other than some minor suggestions LGTM.
src/jit/flowgraph.cpp
Outdated
// If we're jitting the special EqualityComparer<T>.Default | ||
// intrinsic, we can remove the shared helper call if the | ||
// associated field lookup is unused. | ||
if ((info.compFlags & CORINFO_FLG_JIT_INTRINSIC) != 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor suggestion: you might want to say that we are marking it so that later, if it is unused, we can remove it. Otherwise it almost reads as if the code here should be doing the removal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
src/jit/flowgraph.cpp
Outdated
@@ -23074,6 +23087,8 @@ GenTreePtr Compiler::fgInlinePrependStatements(InlineInfo* inlineInfo) | |||
if (argInfo.argHasSideEff) | |||
{ | |||
noway_assert(argInfo.argIsUsed == false); | |||
newStmt = nullptr; | |||
bool noAppend = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: I think it's always better to avoid a double negative, so I would name this append
and initialize it to true.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
src/jit/flowgraph.cpp
Outdated
// This helper call is marked as a "Special DCE" helper during | ||
// importation, over in fgGetStaticsCCtorHelper. | ||
// | ||
// (2) NYI. If after, tunneling through GT_RET_VALs, we find that |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NIT: omit the first ','
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm pretty sure that if the comma after GT_RET_VALs is there, then the first comma needs to be there too, just that it goes after "If" rather than "after" (since "after tunneling through GT_RET_VALs" is the dependent clause, modifying "find", that the commas set apart). But apparently I think that "if the comma after GT_RET_VALs is there" is somehow an exception to that rule...
src/jit/gentree.h
Outdated
@@ -1026,6 +1026,8 @@ struct GenTree | |||
// of the static field; in both of those cases, the constant | |||
// is what gets flagged. | |||
|
|||
#define GTF_ICON_NULLTHIS 0x01000000 // GT_CNS_INT -- null constant is the this in a call, ok to inline call |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: Perhaps it's just me, but I found it difficult to parse and understand this comment. Would it be accurate to say:
// GT_CNS_INT -- This contant is a null that is the 'this' argument to a call; it is ok to inline this call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, this isn't needed anymore, I'll just revert it.
src/jit/gentree.h
Outdated
@@ -4997,8 +5001,7 @@ struct GenTreeStoreInd : public GenTreeIndir | |||
|
|||
struct GenTreeRetExpr : public GenTree | |||
{ | |||
GenTreePtr gtInlineCandidate; | |||
|
|||
GenTreePtr gtInlineCandidate; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While you're here, and since there seems to be consensus on this, you could change GenTreePtr
to GenTree*
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
Am also going to squash things down to one commit when merging... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, just some minor comments.
src/jit/flowgraph.cpp
Outdated
// This helper call is marked as a "Special DCE" helper during | ||
// importation, over in fgGetStaticsCCtorHelper. | ||
// | ||
// (2) NYI. If after, tunneling through GT_RET_VALs, we find that |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm pretty sure that if the comma after GT_RET_VALs is there, then the first comma needs to be there too, just that it goes after "If" rather than "after" (since "after tunneling through GT_RET_VALs" is the dependent clause, modifying "find", that the commas set apart). But apparently I think that "if the comma after GT_RET_VALs is there" is somehow an exception to that rule...
@@ -6415,6 +6415,7 @@ GenTreeLclFld* Compiler::gtNewLclFldNode(unsigned lnum, var_types type, unsigned | |||
} | |||
|
|||
GenTreePtr Compiler::gtNewInlineCandidateReturnExpr(GenTreePtr inlineCandidate, var_types type) | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Revert?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure.
// | ||
// Look for the following tree shapes | ||
// prejit: (IND (ADD (CONST, CALL(special dce helper...)))) | ||
// jit : (COMMA (CALL(special dce helper...), (FIELD ...))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunate that these have different patterns, and jit-diff only shows prejit diffs. Any thoughts about how to add regression testing for the jit case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could possibly catch regressions in a perf test. Or SPMI if we ever get that into CI.
src/jit/importer.cpp
Outdated
// Expect one class generic parameter; figure out which it is. | ||
// | ||
// Probably should do this over on the runtime side now and just | ||
// pass in the method handle. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should you make this change? Create a follow-up issue? Revert this comment?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I should remove the comment.
Have some prospective follow on changes where I just have the type of T
and not the method, and want to figure out the type of the default comparer. So having the jit interface go from type to type is more useful.
} CONTRACTL_END; | ||
|
||
// Mirrors the logic in BCL's CompareHelpers.CreateDefaultEqualityComparer | ||
// And in compile.cpp's SpecializeEqualityComparer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do all three of these places have comments referencing the other two?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not yet, will fix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks Good, with some minor comments
src/vm/jitinterface.cpp
Outdated
|
||
// Special case for byte | ||
// | ||
// Using CLASS__BYTE here doesn't work, figure out why |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you want to leave this comment in?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nah, I'll remove it.
CorElementType normType = elemTypeHnd.GetVerifierCorElementType(); | ||
|
||
switch(normType) | ||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might want reference or include this comment explaining why we have the switch cases below:
from BCL\System\Collections\Generic\EqualityComparer.cs
// Depending on the enum type, we need to special case the comparers so that we avoid boxing
// Note: We have different comparers for Short and SByte because for those types we need to make sure we call GetHashCode on the actual underlying type as the
// implementation of GetHashCode is more complex than for the other types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's already a comment above indicating that the logic here must match what happens when you call CompareHelpers.CreateDefaultEqualityComparer
. You think I need more emphasis on this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had trouble understanding why two cases were done separately and then four other cases were combined. It doesn't really make sense so some explanation is needed here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Enums are special creatures. I'll add a comment.
Perf test results (using same ValueTuple type as above):
Formatting leg hit some kind of network timeout, retrying @dotnet-bot retest Ubuntu x64 Formatting |
Arm LB failure:
retrying. @dotnet-bot retest Ubuntu armlb Cross Release Build |
Since there are now merge conflicts I'm going to rebase and squash locally, then force push. |
Mark `EqualityComparer<T>.Default`'s getter as `[Intrinsic]` so the jit knows there is something special about it. Extend the jit's named intrinsic recognizer to recognize this method. Add a new jit interface method to determine the exact type returned by `EqualityComparer<T>.Default`, given `T`. Compute the return type by mirroring the logic used in the actual implementation. Bail out when `T` is not final as those cases won't simplify down much and lead to code bloat. Invoke this interface method when trying to devirtualize calls where the 'this' object in the call comes from `EqualityComparer<T>.Default`. The devirtualized methods can then be inlined. Since the specific comparer `Equal` and `GetHashCode` methods look more complicated in IL than they really are, mark them with AggressiveInlining attributes. If devirtualization and inlining happen, it is quite likely that the value of the comparer object itself is not used in the body of the comparer. This value comes from a static field cache on the comparer helper. When the comparer value is ignored, the jit removes the field access since it is non-faulting. It also removes the the class init helper that is there to ensure that the (no-longer accessed) field is properly initialized. This helper has relatively high overhead even in the fast case where the class has been initialized aready. Add a perf test. Closes #6688.
0152adf
to
fb5edb0
Compare
Arm failures are almost certainly some kind of infrastructure issue. |
@dotnet/dnceng any idea what is going on with CoreCLR the arm test legs lately? They are all timing out. |
One theory is that the test machines can get oversubscribed leading to timeouts. They seem to be idle now.... @dotnet-bot retest Windows_NT arm Cross Checked Build and Test |
The jit incorporates the value of integer and float typed initonly static fields into its codegen, if the class initializer has already run. The jit can't incorporate the values of ref typed initonly static fields, but the types of those values can't change, and the jit can use this knowledge to enable type based optimizations like devirtualization. In particular for static fields initialized by complex class factory logic the jit can now see the end result of that logic instead of having to try and deduce the type of object that will initialize or did initialize the field. Examples of this factory pattern in include `EqualityComparer<T>.Default` and `Comparer<T>.Default`. The former is already optimized in some cases by via special-purpose modelling in the framework, jit, and runtime (see dotnet#14125) but the latter is not. With this change calls through `Comparer<T>.Default` may now also devirtualize (though won't yet inline as the devirtualization happens late). Addresses #4108.
The jit incorporates the value of integer and float typed initonly static fields into its codegen, if the class initializer has already run. The jit can't incorporate the values of ref typed initonly static fields, but the types of those values can't change, and the jit can use this knowledge to enable type based optimizations like devirtualization. In particular for static fields initialized by complex class factory logic the jit can now see the end result of that logic instead of having to try and deduce the type of object that will initialize or did initialize the field. Examples of this factory pattern in include `EqualityComparer<T>.Default` and `Comparer<T>.Default`. The former is already optimized in some cases by via special-purpose modelling in the framework, jit, and runtime (see dotnet#14125) but the latter is not. With this change calls through `Comparer<T>.Default` may now also devirtualize (though won't yet inline as the devirtualization happens late). Addresses #4108.
The jit incorporates the value of integer and float typed initonly static fields into its codegen, if the class initializer has already run. The jit can't incorporate the values of ref typed initonly static fields, but the types of those values can't change, and the jit can use this knowledge to enable type based optimizations like devirtualization. In particular for static fields initialized by complex class factory logic the jit can now see the end result of that logic instead of having to try and deduce the type of object that will initialize or did initialize the field. Examples of this factory pattern in include `EqualityComparer<T>.Default` and `Comparer<T>.Default`. The former is already optimized in some cases by via special-purpose modelling in the framework, jit, and runtime (see dotnet#14125) but the latter is not. With this change calls through `Comparer<T>.Default` may now also devirtualize (though won't yet inline as the devirtualization happens late). Addresses #4108.
The jit incorporates the value of integer and float typed initonly static fields into its codegen, if the class initializer has already run. The jit can't incorporate the values of ref typed initonly static fields, but the types of those values can't change, and the jit can use this knowledge to enable type based optimizations like devirtualization. In particular for static fields initialized by complex class factory logic the jit can now see the end result of that logic instead of having to try and deduce the type of object that will initialize or did initialize the field. Examples of this factory pattern in include `EqualityComparer<T>.Default` and `Comparer<T>.Default`. The former is already optimized in some cases by via special-purpose modelling in the framework, jit, and runtime (see #14125) but the latter is not. With this change calls through `Comparer<T>.Default` may now also devirtualize (though won't yet inline as the devirtualization happens late). Also update the reflection code to throw an exception instead of changing the value of a fully initialized static readonly field. Closes #4108.
…/coreclr#20886) The jit incorporates the value of integer and float typed initonly static fields into its codegen, if the class initializer has already run. The jit can't incorporate the values of ref typed initonly static fields, but the types of those values can't change, and the jit can use this knowledge to enable type based optimizations like devirtualization. In particular for static fields initialized by complex class factory logic the jit can now see the end result of that logic instead of having to try and deduce the type of object that will initialize or did initialize the field. Examples of this factory pattern in include `EqualityComparer<T>.Default` and `Comparer<T>.Default`. The former is already optimized in some cases by via special-purpose modelling in the framework, jit, and runtime (see dotnet/coreclr#14125) but the latter is not. With this change calls through `Comparer<T>.Default` may now also devirtualize (though won't yet inline as the devirtualization happens late). Also update the reflection code to throw an exception instead of changing the value of a fully initialized static readonly field. Closes dotnet/coreclr#4108. Commit migrated from dotnet/coreclr@c2abe89
Mark
EqualityComparer<T>.Default
's getter as[Intrinsic]
sothe jit knows there is something special about it. Extend the jit's
named intrinsic recognizer to recognize this method.
Add a new jit interface method to determine the exact type returned
by
EqualityComparer<T>.Default
, givenT
. Compute the return type bymirroring the logic used in the actual implementation.
Invoke this interface method when trying to devirtualize calls where
the 'this' object in the call comes from
EqualityComparer<T>.Default
.Handle both the early and late devirtualization possibilties.
The devirtualized method can then be inlined if devirtualization
happens early; inlining uses the normal jit heuristics here.
Closes #6688.