Skip to content

Commit d3f4519

Browse files
authored
Intrinsify Enum.Equals to avoid boxing (#122779)
1 parent c507305 commit d3f4519

6 files changed

Lines changed: 176 additions & 2 deletions

File tree

src/coreclr/jit/gentree.cpp

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13839,6 +13839,50 @@ GenTree* Compiler::gtFoldExprCall(GenTreeCall* call)
1383913839
break;
1384013840
}
1384113841

13842+
case NI_System_Enum_Equals:
13843+
{
13844+
assert(call->AsCall()->gtArgs.CountUserArgs() == 2);
13845+
GenTree* arg0 = call->AsCall()->gtArgs.GetArgByIndex(0)->GetNode();
13846+
GenTree* arg1 = call->AsCall()->gtArgs.GetArgByIndex(1)->GetNode();
13847+
13848+
bool isArg0Exact;
13849+
bool isArg1Exact;
13850+
bool isNonNull; // Unused here.
13851+
13852+
CORINFO_CLASS_HANDLE cls0 = gtGetClassHandle(arg0, &isArg0Exact, &isNonNull);
13853+
CORINFO_CLASS_HANDLE cls1 = gtGetClassHandle(arg1, &isArg1Exact, &isNonNull);
13854+
if ((cls0 != cls1) || (cls0 == NO_CLASS_HANDLE) || !isArg0Exact || !isArg1Exact)
13855+
{
13856+
break;
13857+
}
13858+
13859+
assert(info.compCompHnd->isEnum(cls1, nullptr) == TypeCompareState::Must);
13860+
13861+
CorInfoType corTyp = info.compCompHnd->getTypeForPrimitiveValueClass(cls1);
13862+
if (corTyp == CORINFO_TYPE_UNDEF)
13863+
{
13864+
break;
13865+
}
13866+
13867+
var_types typ = JITtype2varType(corTyp);
13868+
if (!varTypeIsIntegral(typ))
13869+
{
13870+
// Ignore non-integral enums e.g. enums based on float/double
13871+
break;
13872+
}
13873+
13874+
// Unbox both integral arguments and compare their underlying values
13875+
GenTree* offset = gtNewIconNode(TARGET_POINTER_SIZE, TYP_I_IMPL);
13876+
GenTree* addr0 = gtNewOperNode(GT_ADD, TYP_BYREF, arg0, offset);
13877+
GenTree* addr1 = gtNewOperNode(GT_ADD, TYP_BYREF, arg1, gtCloneExpr(offset));
13878+
GenTree* cmpNode = gtNewOperNode(GT_EQ, TYP_INT, gtNewIndir(typ, addr0), gtNewIndir(typ, addr1));
13879+
JITDUMP("Optimized Enum.Equals call to comparison of underlying values:\n");
13880+
DISPTREE(cmpNode);
13881+
JITDUMP("\n");
13882+
13883+
return gtFoldExpr(cmpNode);
13884+
}
13885+
1384213886
case NI_System_Type_op_Equality:
1384313887
case NI_System_Type_op_Inequality:
1384413888
{

src/coreclr/jit/importercalls.cpp

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,6 +1003,13 @@ var_types Compiler::impImportCall(OPCODE opcode,
10031003
if ((callInfo->methodFlags & CORINFO_FLG_INTRINSIC) != 0)
10041004
{
10051005
call->AsCall()->gtCallMoreFlags |= GTF_CALL_M_SPECIAL_INTRINSIC;
1006+
1007+
GenTree* foldedCall = gtFoldExprCall(call->AsCall());
1008+
if ((foldedCall != call) || !call->IsCall())
1009+
{
1010+
impPushOnStack(foldedCall, typeInfo(foldedCall->TypeGet()));
1011+
return foldedCall->TypeGet();
1012+
}
10061013
}
10071014
}
10081015
}
@@ -1571,7 +1578,7 @@ var_types Compiler::impImportCall(OPCODE opcode,
15711578
}
15721579

15731580
//------------------------------------------------------------------------
1574-
// impThrowIfNull: Remove redundandant boxing from ArgumentNullException_ThrowIfNull
1581+
// impThrowIfNull: Remove redundant boxing from ArgumentNullException_ThrowIfNull
15751582
// it is done for Tier0 where we can't remove it without inlining otherwise.
15761583
//
15771584
// Arguments:
@@ -10344,7 +10351,11 @@ NamedIntrinsic Compiler::lookupNamedIntrinsic(CORINFO_METHOD_HANDLE method)
1034410351
{
1034510352
if (strcmp(className, "Enum") == 0)
1034610353
{
10347-
if (strcmp(methodName, "HasFlag") == 0)
10354+
if (strcmp(methodName, "Equals") == 0)
10355+
{
10356+
result = NI_System_Enum_Equals;
10357+
}
10358+
else if (strcmp(methodName, "HasFlag") == 0)
1034810359
{
1034910360
result = NI_System_Enum_HasFlag;
1035010361
}

src/coreclr/jit/namedintrinsiclist.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ enum NamedIntrinsic : unsigned short
1515

1616
NI_System_ArgumentNullException_ThrowIfNull,
1717

18+
NI_System_Enum_Equals,
1819
NI_System_Enum_HasFlag,
1920

2021
NI_System_BitConverter_DoubleToInt64Bits,

src/libraries/System.Private.CoreLib/src/System/Enum.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1187,6 +1187,7 @@ internal object GetValue()
11871187
}
11881188

11891189
/// <inheritdoc/>
1190+
[Intrinsic]
11901191
public override bool Equals([NotNullWhen(true)] object? obj)
11911192
{
11921193
if (obj is null)
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
//
4+
5+
using System;
6+
using System.Runtime.CompilerServices;
7+
using Xunit;
8+
9+
public class EnumIntrinsics
10+
{
11+
public enum SByteEnum : sbyte { Min = sbyte.MinValue, Neg = -1, Zero = 0, Max = sbyte.MaxValue }
12+
public enum ByteEnum : byte { Min = 0, Max = 255 }
13+
public enum ShortEnum : short { Min = short.MinValue, Max = short.MaxValue }
14+
public enum UShortEnum : ushort { Min = 0, Max = ushort.MaxValue }
15+
public enum IntEnum : int { Min = int.MinValue, Zero = 0, Max = int.MaxValue }
16+
public enum UIntEnum : uint { Min = 0, Max = uint.MaxValue }
17+
public enum LongEnum : long { Min = long.MinValue, Max = long.MaxValue }
18+
public enum ULongEnum : ulong { Min = 0, Max = ulong.MaxValue }
19+
[Flags] public enum FlagsEnum { None = 0, A = 1, B = 2, All = 3 }
20+
21+
[Fact]
22+
public static void TestEntryPoint()
23+
{
24+
TestSimpleEnums();
25+
TestGenericEnums();
26+
TestDifferentUnderlyingTypes();
27+
TestCornerCases();
28+
}
29+
30+
[MethodImpl(MethodImplOptions.NoInlining)]
31+
private static void TestSimpleEnums()
32+
{
33+
// Testing bit-pattern identities
34+
Assert.True(SByteEnum.Neg.Equals((SByteEnum)(-1)));
35+
Assert.True(ByteEnum.Max.Equals((ByteEnum)255));
36+
Assert.False(SByteEnum.Max.Equals(SByteEnum.Min));
37+
38+
// Flags behavior (bitwise equality)
39+
FlagsEnum flags = FlagsEnum.A | FlagsEnum.B;
40+
Assert.True(flags.Equals(FlagsEnum.All));
41+
Assert.False(flags.Equals(FlagsEnum.A));
42+
43+
// 64-bit boundaries
44+
Assert.True(ULongEnum.Max.Equals((ULongEnum)ulong.MaxValue));
45+
Assert.True(LongEnum.Min.Equals((LongEnum)long.MinValue));
46+
}
47+
48+
[MethodImpl(MethodImplOptions.NoInlining)]
49+
private static void TestGenericEnums()
50+
{
51+
// Testing generic instantiation for every width
52+
Assert.True(CheckGenericEquals(SByteEnum.Max, SByteEnum.Max));
53+
Assert.True(CheckGenericEquals(UShortEnum.Max, UShortEnum.Max));
54+
Assert.True(CheckGenericEquals(UIntEnum.Max, UIntEnum.Max));
55+
Assert.True(CheckGenericEquals(ULongEnum.Max, ULongEnum.Max));
56+
57+
var container = new GenericEnumClass<IntEnum> { field = IntEnum.Min };
58+
Assert.True(CheckGenericEquals(container.field, IntEnum.Min));
59+
Assert.False(CheckGenericEquals(container.field, IntEnum.Max));
60+
}
61+
62+
[MethodImpl(MethodImplOptions.NoInlining)]
63+
private static bool CheckGenericEquals<T>(T left, T right) where T : Enum => left.Equals(right);
64+
65+
[MethodImpl(MethodImplOptions.NoInlining)]
66+
private static void TestDifferentUnderlyingTypes()
67+
{
68+
// 0xFF pattern
69+
Assert.False(((SByteEnum)(-1)).Equals((ByteEnum)255));
70+
71+
// 0x00 pattern
72+
Assert.False(SByteEnum.Zero.Equals(ByteEnum.Min));
73+
74+
// 0xFFFF pattern
75+
Assert.False(((ShortEnum)(-1)).Equals((UShortEnum)ushort.MaxValue));
76+
77+
// 0xFFFFFFFF pattern
78+
Assert.False(((IntEnum)(-1)).Equals((UIntEnum)uint.MaxValue));
79+
80+
// Signed vs Unsigned same width
81+
Assert.False(IntEnum.Max.Equals((UIntEnum)int.MaxValue));
82+
}
83+
84+
[MethodImpl(MethodImplOptions.NoInlining)]
85+
private static void TestCornerCases()
86+
{
87+
// Ensure no false positives with primitive types (boxing checks)
88+
Assert.False(IntEnum.Zero.Equals(0));
89+
Assert.False(LongEnum.Max.Equals(long.MaxValue));
90+
91+
// Different enum types entirely
92+
Assert.False(SimpleEnum.A.Equals(FlagsEnum.A));
93+
94+
// Null and Object references
95+
object obj = new object();
96+
Assert.False(SimpleEnum.B.Equals(obj));
97+
Assert.False(SimpleEnum.C.Equals(null));
98+
99+
// Double boxing scenarios
100+
object boxedA = SimpleEnum.A;
101+
object boxedB = SimpleEnum.A;
102+
Assert.True(boxedA.Equals(boxedB));
103+
Assert.True(SimpleEnum.A.Equals(boxedB));
104+
}
105+
106+
public class GenericEnumClass<T> where T : Enum { public T field; }
107+
public enum SimpleEnum { A, B, C }
108+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<DebugType>None</DebugType>
4+
<Optimize>True</Optimize>
5+
</PropertyGroup>
6+
<ItemGroup>
7+
<Compile Include="EnumIntrinsics.cs" />
8+
</ItemGroup>
9+
</Project>

0 commit comments

Comments
 (0)