Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/coreclr/jit/compiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -4496,6 +4496,7 @@ class Compiler
IL_OFFSET ilOffset = BAD_IL_OFFSET);

bool impConsiderCallProbe(GenTreeCall* call, IL_OFFSET ilOffset);
GenTree* impFoldEnumEquals(GenTreeCall* call);

enum class GDVProbeType
{
Expand Down
66 changes: 64 additions & 2 deletions src/coreclr/jit/importercalls.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1003,6 +1003,16 @@ var_types Compiler::impImportCall(OPCODE opcode,
if ((callInfo->methodFlags & CORINFO_FLG_INTRINSIC) != 0)
{
call->AsCall()->gtCallMoreFlags |= GTF_CALL_M_SPECIAL_INTRINSIC;

if (call->AsCall()->IsSpecialIntrinsic(this, NI_System_Enum_Equals))
{
GenTree* folded = impFoldEnumEquals(call->AsCall());
if (folded)
{
impPushOnStack(folded, typeInfo(folded->TypeGet()));
return folded->TypeGet();
}
}
}
}
}
Expand Down Expand Up @@ -1571,7 +1581,55 @@ var_types Compiler::impImportCall(OPCODE opcode,
}

//------------------------------------------------------------------------
// impThrowIfNull: Remove redundandant boxing from ArgumentNullException_ThrowIfNull
// impFoldEnumEquals: Optimize Enum.Equals calls by unboxing and comparing underlying values
//
// Arguments:
// call -- call representing Enum.Equals
//
// Return Value:
// Optimized tree (or nullptr if we can't optimize it).
//
GenTree* Compiler::impFoldEnumEquals(GenTreeCall* call)
{
assert(call->IsSpecialIntrinsic(this, NI_System_Enum_Equals));
assert(call->AsCall()->gtArgs.CountUserArgs() == 2);
GenTree* arg0 = call->AsCall()->gtArgs.GetArgByIndex(0)->GetNode();
GenTree* arg1 = call->AsCall()->gtArgs.GetArgByIndex(1)->GetNode();

bool isArg0Exact;
bool isArg1Exact;
bool isNonNull; // Unused here.

CORINFO_CLASS_HANDLE cls0 = gtGetClassHandle(arg0, &isArg0Exact, &isNonNull);
CORINFO_CLASS_HANDLE cls1 = gtGetClassHandle(arg1, &isArg1Exact, &isNonNull);
if ((cls0 != cls1) || (cls0 == NO_CLASS_HANDLE) || !isArg0Exact || !isArg1Exact)
{
return nullptr;
}

assert(info.compCompHnd->isEnum(cls1, nullptr) == TypeCompareState::Must);

var_types typ = JITtype2varType(info.compCompHnd->getTypeForPrimitiveNumericClass(cls1));
if (!varTypeIsIntegral(typ))
{
// Ignore non-integral enums e.g. enums based on float/double
return nullptr;
}

// Unbox both integral arguments and compare their underlying values
GenTree* offset = gtNewIconNode(TARGET_POINTER_SIZE, TYP_I_IMPL);
GenTree* addr0 = gtNewOperNode(GT_ADD, TYP_BYREF, arg0, offset);
GenTree* addr1 = gtNewOperNode(GT_ADD, TYP_BYREF, arg1, gtCloneExpr(offset));
GenTree* cmpNode = gtNewOperNode(GT_EQ, TYP_INT, gtNewIndir(typ, addr0), gtNewIndir(typ, addr1));
JITDUMP("Optimized Enum.Equals call to comparison of underlying values:\n");
DISPTREE(cmpNode);
JITDUMP("\n");

return cmpNode;
}

//------------------------------------------------------------------------
// impThrowIfNull: Remove redundant boxing from ArgumentNullException_ThrowIfNull
// it is done for Tier0 where we can't remove it without inlining otherwise.
//
// Arguments:
Expand Down Expand Up @@ -10344,7 +10402,11 @@ NamedIntrinsic Compiler::lookupNamedIntrinsic(CORINFO_METHOD_HANDLE method)
{
if (strcmp(className, "Enum") == 0)
{
if (strcmp(methodName, "HasFlag") == 0)
if (strcmp(methodName, "Equals") == 0)
{
result = NI_System_Enum_Equals;
}
else if (strcmp(methodName, "HasFlag") == 0)
{
result = NI_System_Enum_HasFlag;
}
Expand Down
1 change: 1 addition & 0 deletions src/coreclr/jit/namedintrinsiclist.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ enum NamedIntrinsic : unsigned short

NI_System_ArgumentNullException_ThrowIfNull,

NI_System_Enum_Equals,
NI_System_Enum_HasFlag,

NI_System_BitConverter_DoubleToInt64Bits,
Expand Down
1 change: 1 addition & 0 deletions src/libraries/System.Private.CoreLib/src/System/Enum.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1187,6 +1187,7 @@ internal object GetValue()
}

/// <inheritdoc/>
[Intrinsic]
public override bool Equals([NotNullWhen(true)] object? obj)
{
if (obj is null)
Expand Down
108 changes: 108 additions & 0 deletions src/tests/JIT/Intrinsics/EnumIntrinsics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
//

using System;
using System.Runtime.CompilerServices;
using Xunit;

public class EnumIntrinsics
{
public enum SByteEnum : sbyte { Min = sbyte.MinValue, Neg = -1, Zero = 0, Max = sbyte.MaxValue }
public enum ByteEnum : byte { Min = 0, Max = 255 }
public enum ShortEnum : short { Min = short.MinValue, Max = short.MaxValue }
public enum UShortEnum : ushort { Min = 0, Max = ushort.MaxValue }
public enum IntEnum : int { Min = int.MinValue, Zero = 0, Max = int.MaxValue }
public enum UIntEnum : uint { Min = 0, Max = uint.MaxValue }
public enum LongEnum : long { Min = long.MinValue, Max = long.MaxValue }
public enum ULongEnum : ulong { Min = 0, Max = ulong.MaxValue }
[Flags] public enum FlagsEnum { None = 0, A = 1, B = 2, All = 3 }

[Fact]
public static void TestEntryPoint()
{
TestSimpleEnums();
TestGenericEnums();
TestDifferentUnderlyingTypes();
TestCornerCases();
}

[MethodImpl(MethodImplOptions.NoInlining)]
private static void TestSimpleEnums()
{
// Testing bit-pattern identities
Assert.True(SByteEnum.Neg.Equals((SByteEnum)(-1)));
Assert.True(ByteEnum.Max.Equals((ByteEnum)255));
Assert.False(SByteEnum.Max.Equals(SByteEnum.Min));

// Flags behavior (bitwise equality)
FlagsEnum flags = FlagsEnum.A | FlagsEnum.B;
Assert.True(flags.Equals(FlagsEnum.All));
Assert.False(flags.Equals(FlagsEnum.A));

// 64-bit boundaries
Assert.True(ULongEnum.Max.Equals((ULongEnum)ulong.MaxValue));
Assert.True(LongEnum.Min.Equals((LongEnum)long.MinValue));
}

[MethodImpl(MethodImplOptions.NoInlining)]
private static void TestGenericEnums()
{
// Testing generic instantiation for every width
Assert.True(CheckGenericEquals(SByteEnum.Max, SByteEnum.Max));
Assert.True(CheckGenericEquals(UShortEnum.Max, UShortEnum.Max));
Assert.True(CheckGenericEquals(UIntEnum.Max, UIntEnum.Max));
Assert.True(CheckGenericEquals(ULongEnum.Max, ULongEnum.Max));

var container = new GenericEnumClass<IntEnum> { field = IntEnum.Min };
Assert.True(CheckGenericEquals(container.field, IntEnum.Min));
Assert.False(CheckGenericEquals(container.field, IntEnum.Max));
}

[MethodImpl(MethodImplOptions.NoInlining)]
private static bool CheckGenericEquals<T>(T left, T right) where T : Enum => left.Equals(right);

[MethodImpl(MethodImplOptions.NoInlining)]
private static void TestDifferentUnderlyingTypes()
{
// 0xFF pattern
Assert.False(((SByteEnum)(-1)).Equals((ByteEnum)255));

// 0x00 pattern
Assert.False(SByteEnum.Zero.Equals(ByteEnum.Min));

// 0xFFFF pattern
Assert.False(((ShortEnum)(-1)).Equals((UShortEnum)ushort.MaxValue));

// 0xFFFFFFFF pattern
Assert.False(((IntEnum)(-1)).Equals((UIntEnum)uint.MaxValue));

// Signed vs Unsigned same width
Assert.False(IntEnum.Max.Equals((UIntEnum)int.MaxValue));
}

[MethodImpl(MethodImplOptions.NoInlining)]
private static void TestCornerCases()
{
// Ensure no false positives with primitive types (boxing checks)
Assert.False(IntEnum.Zero.Equals(0));
Assert.False(LongEnum.Max.Equals(long.MaxValue));

// Different enum types entirely
Assert.False(SimpleEnum.A.Equals(FlagsEnum.A));

// Null and Object references
object obj = new object();
Assert.False(SimpleEnum.B.Equals(obj));
Assert.False(SimpleEnum.C.Equals(null));

// Double boxing scenarios
object boxedA = SimpleEnum.A;
object boxedB = SimpleEnum.A;
Assert.True(boxedA.Equals(boxedB));
Assert.True(SimpleEnum.A.Equals(boxedB));
}

public class GenericEnumClass<T> where T : Enum { public T field; }
public enum SimpleEnum { A, B, C }
}
9 changes: 9 additions & 0 deletions src/tests/JIT/Intrinsics/EnumIntrinsics.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<DebugType>None</DebugType>
<Optimize>True</Optimize>
</PropertyGroup>
<ItemGroup>
<Compile Include="EnumIntrinsics.cs" />
</ItemGroup>
</Project>
Loading